|
4801
|
174
|
14
|
2026-05-07T14:30:03.714032+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164203714_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
ActivityLateMoreNotion CalendarcaltVIewWindowJimin ActivityLateMoreNotion CalendarcaltVIewWindowJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovalik y...#:Apps-T lira Cloud• Toastmelt"rreleases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled APP5-20 PMO rclerment SuccessfullProlect: aooWhen: 05/07/2026 14:29-40lag.View JobMessage #releasesAarou ve now conmuncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueidesion nanos on.May 2026 Week19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)07:0010:00 Daily - Platform 093Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne113:0015:00 allSunnort Dailv 1Erol14:00117:00(17:30)20:00|Opus 4.7 AdaptiveClaude ic Aland can make mictakas Plasce double-chork racnonco.Wed oThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmomSave response status for repeat event• This eventAll eventsLeave a note (ootional)CancelSunnort Daily 15:001• Lukas/Stefka 121Sunnort Daily 15:00|Sat9Lukas sterka 121 • now100% 12Inu/ May 1/-30'03• Search eventsSun 10Lukas/Stetka 127117:20-18:00 20milJoin Google MeetUseful shortcutsMenu bar calendarcontrol* KTogale sidebarGo to dateAll keyboard snortcutsAlexieva kideGet Calendar to go...
|
NULL
|
-6377186923312846226
|
NULL
|
click
|
ocr
|
NULL
|
ActivityLateMoreNotion CalendarcaltVIewWindowJimin ActivityLateMoreNotion CalendarcaltVIewWindowJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovalik y...#:Apps-T lira Cloud• Toastmelt"rreleases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled APP5-20 PMO rclerment SuccessfullProlect: aooWhen: 05/07/2026 14:29-40lag.View JobMessage #releasesAarou ve now conmuncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueidesion nanos on.May 2026 Week19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)07:0010:00 Daily - Platform 093Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne113:0015:00 allSunnort Dailv 1Erol14:00117:00(17:30)20:00|Opus 4.7 AdaptiveClaude ic Aland can make mictakas Plasce double-chork racnonco.Wed oThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmomSave response status for repeat event• This eventAll eventsLeave a note (ootional)CancelSunnort Daily 15:001• Lukas/Stefka 121Sunnort Daily 15:00|Sat9Lukas sterka 121 • now100% 12Inu/ May 1/-30'03• Search eventsSun 10Lukas/Stetka 127117:20-18:00 20milJoin Google MeetUseful shortcutsMenu bar calendarcontrol* KTogale sidebarGo to dateAll keyboard snortcutsAlexieva kideGet Calendar to go...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4802
|
174
|
15
|
2026-05-07T14:30:06.910839+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164206910_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
ActivityLateMoreNotion CalendarcaltVIewWindowJimin ActivityLateMoreNotion CalendarcaltVIewWindowJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovalik y...#:Apps-T lira Cloud• Toastmelt"rreleases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled APP5-20 DMO rclerment SuccessfullProlect: aooWhen: 05/07/2026 14:29-40lag.View JobMessage #releasesAarou ve now conmuncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueidesion nanos on.May 2026 Week19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)07:0010:00 Daily - Platform 093Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne113:0015:00 allSunnort Dailv 1Erol14:00117:00(17:30)20:00|Opus 4.7 AdaptiveClaude ic Aland can make mictakas Plasce double-chork racnonco.Wed oThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmomSave response status for repeat event• This eventAll eventsLeave a note (optiona))Cancelsave responseSunnort Daily 15:00• Lukas/Stefka 121Sunnort Daily 15:00.Sat9Lukas sterka 121 • now100% 12Inu/ May 1/.30:00Week v• Search eventsSun 10Lukas/Stetka 127117:20-18:00 20milUseful shortcutsMenu bar calendarcontrol* KTogale sidebarGo to dateAll keyboard snortcutsAlexieva kideGet Calendar to go...
|
NULL
|
-1462476618109401983
|
NULL
|
click
|
ocr
|
NULL
|
ActivityLateMoreNotion CalendarcaltVIewWindowJimin ActivityLateMoreNotion CalendarcaltVIewWindowJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovalik y...#:Apps-T lira Cloud• Toastmelt"rreleases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled APP5-20 DMO rclerment SuccessfullProlect: aooWhen: 05/07/2026 14:29-40lag.View JobMessage #releasesAarou ve now conmuncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueidesion nanos on.May 2026 Week19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)07:0010:00 Daily - Platform 093Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne113:0015:00 allSunnort Dailv 1Erol14:00117:00(17:30)20:00|Opus 4.7 AdaptiveClaude ic Aland can make mictakas Plasce double-chork racnonco.Wed oThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmomSave response status for repeat event• This eventAll eventsLeave a note (optiona))Cancelsave responseSunnort Daily 15:00• Lukas/Stefka 121Sunnort Daily 15:00.Sat9Lukas sterka 121 • now100% 12Inu/ May 1/.30:00Week v• Search eventsSun 10Lukas/Stetka 127117:20-18:00 20milUseful shortcutsMenu bar calendarcontrol* KTogale sidebarGo to dateAll keyboard snortcutsAlexieva kideGet Calendar to go...
|
4801
|
NULL
|
NULL
|
NULL
|
|
4803
|
173
|
21
|
2026-05-07T14:30:07.002609+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164207002_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEdit→ CViewHistoryBookmarksProfilesTool FirefoxFileEdit→ CViewHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com• | Lukas/Stefka 121 - now100% C47 8• Thu 7 May 17:30:07Lukas/Stefka 121now - 17:30-18:00Ci Join Google MeetI' MeetLukas KovalikLukas/Stefka 121No one else is here* Use Gemini to take notesShare notes and transcriptStartJoin nowOther ways to joinI LakyLak bos...4 System Defa...• FaceTime HD...g Backgrounds...Gemini is available in Meet as your personal in-meeting assistant. It can analyze conversation via temporaryaccess to meeting captions. Using Ask Gemini won't create a recording or store meeting data. The meetinghost can turn it off. Learn moreLộ3...
|
NULL
|
6578741716980121617
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEdit→ CViewHistoryBookmarksProfilesTool FirefoxFileEdit→ CViewHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com• | Lukas/Stefka 121 - now100% C47 8• Thu 7 May 17:30:07Lukas/Stefka 121now - 17:30-18:00Ci Join Google MeetI' MeetLukas KovalikLukas/Stefka 121No one else is here* Use Gemini to take notesShare notes and transcriptStartJoin nowOther ways to joinI LakyLak bos...4 System Defa...• FaceTime HD...g Backgrounds...Gemini is available in Meet as your personal in-meeting assistant. It can analyze conversation via temporaryaccess to meeting captions. Using Ask Gemini won't create a recording or store meeting data. The meetinghost can turn it off. Learn moreLộ3...
|
4800
|
NULL
|
NULL
|
NULL
|
|
4804
|
174
|
16
|
2026-05-07T14:30:08.957887+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164208957_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
ActivityLateMoreNotion CalendarcaltVIewWindowJimin ActivityLateMoreNotion CalendarcaltVIewWindowJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovalik y...#:Apps-T lira Cloud• Toastmelt"rreleases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled ApP5-20 DMO rclerment SuccessfullProlect: apoWhen: 05/07/2026 14-29-40lag.View JobMessage #releasesAarou ve now conmuncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueidesion nanos on.May 2026 Week19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)Stovan Tanev (PTO - 4 days))Tue 5WedoThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmom07:0008:0010:00 Daily - Platform 09:13:00115:00 aliSunnort Dailv isrod16:00117:00(17:30Sunnort Daily 15:001Sunnort Daily 15:001Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne1Lukas/Stefka 121Opus 4.7 AdaptiveClaude ic Aland can make mictakas Plasce double-chork racnonco.20:00|Alexieva kidk14:00-15:00SatLukas sterka 121 • now100% L2Inu/ May 1/.30:09Week v-a Search eventsSun 10Lukas/Stetka 121117:30-18:00 30minJoin Google MeetUseful shortcutsCommand menuMenu oar calendarcontrol* KTogale sidebarGo to dateAll keyboard shortcuts@ Invite acceptedGet Calendar to go...
|
NULL
|
-3265858040992343340
|
NULL
|
visual_change
|
ocr
|
NULL
|
ActivityLateMoreNotion CalendarcaltVIewWindowJimin ActivityLateMoreNotion CalendarcaltVIewWindowJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovalik y...#:Apps-T lira Cloud• Toastmelt"rreleases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled ApP5-20 DMO rclerment SuccessfullProlect: apoWhen: 05/07/2026 14-29-40lag.View JobMessage #releasesAarou ve now conmuncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueidesion nanos on.May 2026 Week19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)Stovan Tanev (PTO - 4 days))Tue 5WedoThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmom07:0008:0010:00 Daily - Platform 09:13:00115:00 aliSunnort Dailv isrod16:00117:00(17:30Sunnort Daily 15:001Sunnort Daily 15:001Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne1Lukas/Stefka 121Opus 4.7 AdaptiveClaude ic Aland can make mictakas Plasce double-chork racnonco.20:00|Alexieva kidk14:00-15:00SatLukas sterka 121 • now100% L2Inu/ May 1/.30:09Week v-a Search eventsSun 10Lukas/Stetka 121117:30-18:00 30minJoin Google MeetUseful shortcutsCommand menuMenu oar calendarcontrol* KTogale sidebarGo to dateAll keyboard shortcuts@ Invite acceptedGet Calendar to go...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4805
|
173
|
22
|
2026-05-07T14:30:10.161365+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164210161_m1.jpg...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
True
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
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...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Lukas/Stefka 121","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-5766662800823142283
|
739158050490045138
|
click
|
hybrid
|
NULL
|
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
FirefoxFileEditViewHistoryBookmarksProfilesCToolsWindowHelplahlmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% <478•Thu 7 May 17:30:12Lukas/Stefka 121now - 17:30-18:00Ci Join Google Meet+...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4806
|
174
|
17
|
2026-05-07T14:30:11.068296+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164211068_m2.jpg...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
True
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
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...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Lukas/Stefka 121","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.016123671,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.27094415,"top":1.0,"width":0.004986702,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.27310506,"top":1.0,"width":0.010638298,"height":-0.086193085},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-1515564132911898322
|
721145986454672002
|
click
|
hybrid
|
NULL
|
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
WindowActivityLateMoreJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovalik y...#:Apps-T lira Cloud• Toast# releases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled ApP5-20 DMO rclerment SuccessfullProlect: apoWhen: 05/07/2026 14:29•40lag.View JobMessage #releasesAarou ve now conmuncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueidesion nanos on.May 2026 Week 19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)Stovan Tanev (PTO - 4 days))Tue 5WedoThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmom10:00 Daily - Platform 09:13:0015:00 aliSunnort Dailv isrod16:0017:00(17:30Sunnort Daily 15:001Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne1Sunnort Daily 15:001Lukas/Stefka 121Opus 4.7 AdaptiveClaude ic Aland can make mictakas Plasce double-chork racnonco.20:00|SatLukas sterka 121 • now100% L2• Inu / May 1/.30-12Week vToday-a Search eventsSun 10Lukas/Stetka 12117:30-18:00 30minJoin Google MeetUseful shortcutsMenu bar calendarcontrol* KTogale sidebarGo to dateAll keyboara snortcutsAlexieva kidk14:00-15:00Get Calendar to go...
|
4804
|
NULL
|
NULL
|
NULL
|
|
4807
|
173
|
23
|
2026-05-07T14:30:13.645762+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164213645_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditView→CHistoryBookmarksProfilesTools FirefoxFileEditView→CHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% <478•Thu 7 May 17:30:13...
|
NULL
|
-5556889322621313911
|
NULL
|
visual_change
|
ocr
|
NULL
|
FirefoxFileEditView→CHistoryBookmarksProfilesTools FirefoxFileEditView→CHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% <478•Thu 7 May 17:30:13...
|
4805
|
NULL
|
NULL
|
NULL
|
|
4808
|
173
|
24
|
2026-05-07T14:30:15.221080+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164215221_m1.jpg...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
True
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
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
1
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.
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:30
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn on 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
Turn off camera (⌘ + e)
Your microphone is off....
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Lukas/Stefka 121","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":14,"bounds":{"left":0.9184028,"top":0.08722222,"width":0.039930556,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":21,"bounds":{"left":0.9461806,"top":0.09888889,"width":0.0038194444,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":13,"bounds":{"left":0.9638889,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Gemini","depth":16,"bounds":{"left":0.96666664,"top":0.09888889,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"bounds":{"left":0.099305555,"top":0.63555557,"width":0.14652778,"height":0.08888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"bounds":{"left":0.020833334,"top":0.65,"width":0.08090278,"height":0.018888889},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"bounds":{"left":0.013194445,"top":0.64611113,"width":0.11076389,"height":0.05666667},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"bounds":{"left":0.47256944,"top":0.5038889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Backgrounds and effects","depth":13,"bounds":{"left":0.503125,"top":0.5038889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Lukas Kovalik","depth":13,"bounds":{"left":0.53368056,"top":0.5061111,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.05590278,"top":0.87166667,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5:30","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.022916667,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PM","depth":12,"bounds":{"left":0.07673611,"top":0.9444444,"width":0.016666668,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Lukas/Stefka 121","depth":12,"bounds":{"left":0.11076389,"top":0.9111111,"width":0.08472222,"height":0.08888888},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas/Stefka 121","depth":15,"bounds":{"left":0.11076389,"top":0.9444444,"width":0.08472222,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":13,"bounds":{"left":0.32118055,"top":0.9288889,"width":0.06111111,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on microphone","depth":13,"bounds":{"left":0.34895834,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"bounds":{"left":0.38784721,"top":0.9288889,"width":0.06111111,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off camera","depth":13,"bounds":{"left":0.415625,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Share screen","depth":12,"bounds":{"left":0.45451388,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Send a reaction","depth":12,"bounds":{"left":0.49895832,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on captions","depth":13,"bounds":{"left":0.5434028,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Raise hand (ctrl + ⌘ + h)","depth":12,"bounds":{"left":0.58784723,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options","depth":12,"bounds":{"left":0.6322917,"top":0.9288889,"width":0.025,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Leave call","depth":12,"bounds":{"left":0.6628472,"top":0.9288889,"width":0.05,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Meeting details","depth":12,"bounds":{"left":0.89166665,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat with everyone","depth":12,"bounds":{"left":0.925,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Meeting tools","depth":12,"bounds":{"left":0.9583333,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Turn off camera (⌘ + e)","depth":10,"bounds":{"left":0.39618057,"top":0.9066667,"width":0.071875,"height":0.013888889},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Your microphone is off.","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-1831973845244514126
|
-6354325647243281544
|
click
|
hybrid
|
NULL
|
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
1
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.
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:30
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn on 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
Turn off camera (⌘ + e)
Your microphone is off.
FirefoxFileEditViewHistoryBookmarks•→Profiles ToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% C8•Thu 7 May 17:30:15=+Lukas Kovalik5:30 PM | Lukas/Stefka 121...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4809
|
174
|
18
|
2026-05-07T14:30:15.221089+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164215221_m2.jpg...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
True
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
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
1
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.
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:30
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn on 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
Turn off camera (⌘ + e)
Your microphone is off....
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Lukas/Stefka 121","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.016123671,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.27094415,"top":1.0,"width":0.004986702,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.27310506,"top":1.0,"width":0.010638298,"height":-0.086193085},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":14,"bounds":{"left":0.70994014,"top":1.0,"width":0.019115692,"height":-0.06264961},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":21,"bounds":{"left":0.72323805,"top":1.0,"width":0.0018284575,"height":-0.071029544},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":13,"bounds":{"left":0.73171544,"top":1.0,"width":0.011968086,"height":-0.06264961},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Gemini","depth":16,"bounds":{"left":0.7330452,"top":1.0,"width":0.013464096,"height":-0.071029544},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pop out this video More screens are more fun. Play this video while you do other things.","depth":15,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"Pop out this video","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"More screens are more fun. Play this video while you do other things.","depth":16,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Backgrounds and effects","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Lukas Kovalik","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5:30","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PM","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Lukas/Stefka 121","depth":12,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas/Stefka 121","depth":15,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on microphone","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn off camera","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Share screen","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Send a reaction","depth":12,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on captions","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Raise hand (ctrl + ⌘ + h)","depth":12,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Leave call","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Meeting details","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat with everyone","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Meeting tools","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Turn off camera (⌘ + e)","depth":10,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Your microphone is off.","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-1831973845244514126
|
-6354325647243281544
|
click
|
hybrid
|
NULL
|
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
1
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.
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:30
PM
Lukas/Stefka 121
Lukas/Stefka 121
Audio settings
Turn on 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
Turn off camera (⌘ + e)
Your microphone is off.
WindowActivityLateMoreJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovalik y...#:Apps-T lira Cloud• Toast# releases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled ApP5-20 DMO rclerment SuccessfullProlect: apoWhen: 05/07/2026 14:29•40lag.View JobMessage #releasesAarou ve now conmuncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueidesion nanos on.May 2026 Week 19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)Stovan Tanev (PTO - 4 days))Tue 5WedoThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmom10:00 Daily - Platform 09:13:0015:00 aliSunnort Dailv isrod16:0017:00(17:30Sunnort Daily 15:001Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne1Sunnort Daily 15:001Lukas/Stefka 121Opus 4.7 AdaptiveClaude ic Aland can make mictakas Plasce double-chork racnonco.20:00|SatLukas sterka 121 • now100% L2• Inu / May 1/.30.10Week vToday-a Search eventsSun 10Lukas/Stetka 12117:30-18:00 30minJoin Google MeetUseful shortcutsMenu bar calendarcontrol* KTogale sidebarGo to dateAll keyboara snortcutsAlexieva kidk14:00-15:00Get Calendar to go...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4810
|
173
|
25
|
2026-05-07T14:30:16.527207+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164216527_m1.jpg...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
True
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
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
1
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Lukas/Stefka 121","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":14,"bounds":{"left":0.88854164,"top":0.08722222,"width":0.039930556,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":21,"bounds":{"left":0.91631943,"top":0.09888889,"width":0.0038194444,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":13,"bounds":{"left":0.93333334,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":16,"bounds":{"left":0.9361111,"top":0.09888889,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":14,"bounds":{"left":0.9638889,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Gemini","depth":21,"bounds":{"left":0.96666664,"top":0.09888889,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"bounds":{"left":0.47256944,"top":0.5038889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Backgrounds and effects","depth":13,"bounds":{"left":0.503125,"top":0.5038889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Lukas Kovalik","depth":13,"bounds":{"left":0.53368056,"top":0.5061111,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.05590278,"top":0.87166667,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
3533407810075739653
|
7638346295813100074
|
visual_change
|
hybrid
|
NULL
|
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
1
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
FirefoxFileEditViewHistoryBookmarks•→Profiles ToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% C8•Thu 7 May 17:30:16=1+Lukas Kovalik5:30 PM | Lukas/Stefka 121...
|
4808
|
NULL
|
NULL
|
NULL
|
|
4811
|
173
|
26
|
2026-05-07T14:30:19.703814+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164219703_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarks•→Profiles Tool FirefoxFileEditViewHistoryBookmarks•→Profiles ToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.comLukas/Stefka 121 - now100% C8•Thu 7 May 17:30:19=+Lukas Kovalik5:30 PM | Lukas/Stefka 121...
|
NULL
|
3395110732402002013
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarks•→Profiles Tool FirefoxFileEditViewHistoryBookmarks•→Profiles ToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.comLukas/Stefka 121 - now100% C8•Thu 7 May 17:30:19=+Lukas Kovalik5:30 PM | Lukas/Stefka 121...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4812
|
174
|
19
|
2026-05-07T14:30:19.704155+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164219704_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
WindowActivityLateMoreJiminny... v# engineering# g WindowActivityLateMoreJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovalik y...#:Apps-T lira Cloud• Toast# releases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled ApP5-20 DMO rclerment SuccessfullProlect: apoWhen: 05/07/2026 14:29•40lag.View JobMessage #releasesAarou ve now conmuncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueidesion nanos on.May 2026 Week 19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)Stovan Tanev (PTO - 4 days))Tue 5WedoThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmom10:00 Daily - Platform 09:13:0015:00 aliSunnort Dailv isrod16:0017:00(17:30Sunnort Daily 15:001Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne1Sunnort Daily 15:001Lukas/Stefka 121Opus 4.7 AdaptiveClaude ic Aland can make mictakas Plasce double-chork racnonco.20:00|SatLukas sterka 121 • now100% L2• Inu / May 1/.30-19Week vToday-a Search eventsSun 10Lukas/Stetka 12117:30-18:00 30minJoin Google MeetUseful shortcutsMenu bar calendarcontrol* KTogale sidebarGo to dateAll keyboara snortcutsAlexieva kidk14:00-15:00Get Calendar to go...
|
NULL
|
-965411602106709705
|
NULL
|
click
|
ocr
|
NULL
|
WindowActivityLateMoreJiminny... v# engineering# g WindowActivityLateMoreJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovalik y...#:Apps-T lira Cloud• Toast# releases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled ApP5-20 DMO rclerment SuccessfullProlect: apoWhen: 05/07/2026 14:29•40lag.View JobMessage #releasesAarou ve now conmuncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueidesion nanos on.May 2026 Week 19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)Stovan Tanev (PTO - 4 days))Tue 5WedoThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmom10:00 Daily - Platform 09:13:0015:00 aliSunnort Dailv isrod16:0017:00(17:30Sunnort Daily 15:001Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne1Sunnort Daily 15:001Lukas/Stefka 121Opus 4.7 AdaptiveClaude ic Aland can make mictakas Plasce double-chork racnonco.20:00|SatLukas sterka 121 • now100% L2• Inu / May 1/.30-19Week vToday-a Search eventsSun 10Lukas/Stetka 12117:30-18:00 30minJoin Google MeetUseful shortcutsMenu bar calendarcontrol* KTogale sidebarGo to dateAll keyboara snortcutsAlexieva kidk14:00-15:00Get Calendar to go...
|
4809
|
NULL
|
NULL
|
NULL
|
|
4813
|
173
|
27
|
2026-05-07T14:30:22.348642+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164222348_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfiles ToolsW FirefoxFileEditViewHistoryBookmarksProfiles ToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.cc| Lukas/Stefka 121 • nowAll-In-One( Capture AreaC: Capture Previous Area• Capture Fullscreen• Capture Windowi Scrolling Capture• Self-TimerAa Capture Text (OCR)• Record ScreenHide Desktop Icons• Open...& Pin to the Screen...Capturg istory...About CleanShot...Check for Updates...Settings...QuitA+₴5100% <478•Thu 7 May 17:30:22+₴ 3TKTOTR38,Lukas Kovalik5:30 PM | Lukas/Stefka 121...
|
NULL
|
-8774004102531481801
|
NULL
|
visual_change
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfiles ToolsW FirefoxFileEditViewHistoryBookmarksProfiles ToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.cc| Lukas/Stefka 121 • nowAll-In-One( Capture AreaC: Capture Previous Area• Capture Fullscreen• Capture Windowi Scrolling Capture• Self-TimerAa Capture Text (OCR)• Record ScreenHide Desktop Icons• Open...& Pin to the Screen...Capturg istory...About CleanShot...Check for Updates...Settings...QuitA+₴5100% <478•Thu 7 May 17:30:22+₴ 3TKTOTR38,Lukas Kovalik5:30 PM | Lukas/Stefka 121...
|
4811
|
NULL
|
NULL
|
NULL
|
|
4814
|
174
|
20
|
2026-05-07T14:30:30.652224+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164230652_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
WindowActivityLateMoreJiminny... v# engineering# g WindowActivityLateMoreJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovalik y...#:Apps-T lira Cloud• Toast# releases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled ApP5-20 DMO rclerment SuccessfullProlect: apoWhen: 05/07/2026 14:29•40lag.View JobMessage #releasesAarou ve now conmuncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueidesion nanos on.May 2026 Week 19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)Stovan Tanev (PTO - 4 days))Tue 5WedoThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmom10:00 Daily - Platform 09:13:0015:00 aliSunnort Dailv isrod16:0017:00(17:30Sunnort Daily 15:001Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne1Sunnort Daily 15:001Lukas/Stefka 121Opus 4.7 AdaptiveClaude ic Aland can make mictakas Plasce double-chork racnonco.20:00|SatLukas sterka 121 • now100% L2• Inu / May 1/.30•30Week vToday-a Search eventsSun 10Lukas/Stetka 12117:30-18:00 30minJoin Google MeetUseful shortcutsMenu bar calendarcontrol* KTogale sidebarGo to dateAll keyboara snortcutsAlexieva kidk14:00-15:00Get Calendar to go...
|
NULL
|
1614650520817092725
|
NULL
|
click
|
ocr
|
NULL
|
WindowActivityLateMoreJiminny... v# engineering# g WindowActivityLateMoreJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovalik y...#:Apps-T lira Cloud• Toast# releases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled ApP5-20 DMO rclerment SuccessfullProlect: apoWhen: 05/07/2026 14:29•40lag.View JobMessage #releasesAarou ve now conmuncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueidesion nanos on.May 2026 Week 19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)Stovan Tanev (PTO - 4 days))Tue 5WedoThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmom10:00 Daily - Platform 09:13:0015:00 aliSunnort Dailv isrod16:0017:00(17:30Sunnort Daily 15:001Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne1Sunnort Daily 15:001Lukas/Stefka 121Opus 4.7 AdaptiveClaude ic Aland can make mictakas Plasce double-chork racnonco.20:00|SatLukas sterka 121 • now100% L2• Inu / May 1/.30•30Week vToday-a Search eventsSun 10Lukas/Stetka 12117:30-18:00 30minJoin Google MeetUseful shortcutsMenu bar calendarcontrol* KTogale sidebarGo to dateAll keyboara snortcutsAlexieva kidk14:00-15:00Get Calendar to go...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4815
|
173
|
28
|
2026-05-07T14:30:30.750385+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164230750_m1.jpg...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
True
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
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
1
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Lukas/Stefka 121","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88784724,"top":0.08722222,"width":0.039930556,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":22,"bounds":{"left":0.915625,"top":0.09888889,"width":0.0038194444,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.09888889,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":15,"bounds":{"left":0.9638889,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Gemini","depth":22,"bounds":{"left":0.96666664,"top":0.09888889,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"bounds":{"left":0.47256944,"top":0.5038889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Backgrounds and effects","depth":13,"bounds":{"left":0.503125,"top":0.5038889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Lukas Kovalik","depth":13,"bounds":{"left":0.53368056,"top":0.5061111,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.05590278,"top":0.87166667,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
3533407810075739653
|
7638346295813100074
|
click
|
hybrid
|
NULL
|
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
1
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
FirefoxFileEditViewHistoryBookmarksProfiles ToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% C8•Thu 7 May 17:30:31Lukas Kovalik5:30 PM | Lukas/Stefka 121...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4816
|
174
|
21
|
2026-05-07T14:30:32.545426+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164232545_m2.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
ActivityFllesLateJiminny….v# engineering# general# ActivityFllesLateJiminny….v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-ofhce# support# thank-vous# the people of jimi.ó- Direct messages(3 Aneliya Angelova. ...8 Stovan Tanev €Stefka StoyanovaVes. Galya Dimitrova. Aneliva Angelova8. Vasil Vasilev¿ James Graham8. Nikolay Ivanov• Lukas Kovali..#: Apps-T lira Cloud• Toastaeston nanos oniWrite a message.sr releases9 22Messagesr Files• BookmarksTodayView JobCircleCl APP 4:32 PMNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-209(https://jiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-205(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fix-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fix-deleting-old-trackso) uimi,nnvlann Added bv GitHublCircled App 5-20 pMI• Deployment Successful!Prolect: appWhen: 05/07/2026 14:29-40lagiView JobMessage wreleasesMay 2026 Week1907:0010:00JDaily - Platform 093Draa to record a part of the screen. Press LW to select a window.15:00 all sunnort DailvislOpus 4.7 AdaptiveAlways inspect policyName on 429 to knoihack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results ner query.• Ratch enânoints. tin to 100 records ne1(17:30)Thu 7stoyan lomov (Plo" & days)'Sunnort Dailv 15.Lukas/Stefka 121етка 121Week vTodaySun1100% 57Inu / May 1/.-a Search eventsLukas/Stetka 121Useful shortcutscontrol * KAlexieva kideGet Calendar to go...
|
NULL
|
1251299794163991857
|
NULL
|
click
|
ocr
|
NULL
|
ActivityFllesLateJiminny….v# engineering# general# ActivityFllesLateJiminny….v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-ofhce# support# thank-vous# the people of jimi.ó- Direct messages(3 Aneliya Angelova. ...8 Stovan Tanev €Stefka StoyanovaVes. Galya Dimitrova. Aneliva Angelova8. Vasil Vasilev¿ James Graham8. Nikolay Ivanov• Lukas Kovali..#: Apps-T lira Cloud• Toastaeston nanos oniWrite a message.sr releases9 22Messagesr Files• BookmarksTodayView JobCircleCl APP 4:32 PMNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-209(https://jiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-205(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fix-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fix-deleting-old-trackso) uimi,nnvlann Added bv GitHublCircled App 5-20 pMI• Deployment Successful!Prolect: appWhen: 05/07/2026 14:29-40lagiView JobMessage wreleasesMay 2026 Week1907:0010:00JDaily - Platform 093Draa to record a part of the screen. Press LW to select a window.15:00 all sunnort DailvislOpus 4.7 AdaptiveAlways inspect policyName on 429 to knoihack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results ner query.• Ratch enânoints. tin to 100 records ne1(17:30)Thu 7stoyan lomov (Plo" & days)'Sunnort Dailv 15.Lukas/Stefka 121етка 121Week vTodaySun1100% 57Inu / May 1/.-a Search eventsLukas/Stetka 121Useful shortcutscontrol * KAlexieva kideGet Calendar to go...
|
4814
|
NULL
|
NULL
|
NULL
|
|
4817
|
173
|
29
|
2026-05-07T14:30:32.952017+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164232952_m1.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEdit View→ CHistoryBookmarksProfiles To FirefoxFileEdit View→ CHistoryBookmarksProfiles ToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% CThu 7 May 17:30:331440900Record GIFRecord VideoTFLSLukas Kovalik5:30 PM | Lukas/Stefka 121...
|
NULL
|
-6626976699248636614
|
NULL
|
visual_change
|
ocr
|
NULL
|
FirefoxFileEdit View→ CHistoryBookmarksProfiles To FirefoxFileEdit View→ CHistoryBookmarksProfiles ToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% CThu 7 May 17:30:331440900Record GIFRecord VideoTFLSLukas Kovalik5:30 PM | Lukas/Stefka 121...
|
4815
|
NULL
|
NULL
|
NULL
|
|
4818
|
173
|
30
|
2026-05-07T14:30:34.735519+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164234735_m1.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfiles• .→Too FirefoxFileEditViewHistoryBookmarksProfiles• .→ToolsWindowHelp(ah|meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% C8Thu 7 May 17:30:34 •1387827*(GIF)Record GIFtFRecord VideoTSLukas Kovalik5:30 PMLukas/Stefka 121Lộ3...
|
NULL
|
-2497751894440205603
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfiles• .→Too FirefoxFileEditViewHistoryBookmarksProfiles• .→ToolsWindowHelp(ah|meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% C8Thu 7 May 17:30:34 •1387827*(GIF)Record GIFtFRecord VideoTSLukas Kovalik5:30 PMLukas/Stefka 121Lộ3...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4819
|
173
|
31
|
2026-05-07T14:30:35.404493+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164235404_m1.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfiles→ToolsW FirefoxFileEditViewHistoryBookmarksProfiles→ToolsWindowHelp(ah|meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% C8Thu 7 May 17:30:35 •1387826*(GIF)Record GIFRecord VideoTFLukas Kovalik5:30 PMLukas/Stefka 121Lộ3...
|
NULL
|
-6153095882341645874
|
NULL
|
click
|
ocr
|
NULL
|
FirefoxFileEditViewHistoryBookmarksProfiles→ToolsW FirefoxFileEditViewHistoryBookmarksProfiles→ToolsWindowHelp(ah|meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% C8Thu 7 May 17:30:35 •1387826*(GIF)Record GIFRecord VideoTFLukas Kovalik5:30 PMLukas/Stefka 121Lộ3...
|
4818
|
NULL
|
NULL
|
NULL
|
|
4820
|
174
|
22
|
2026-05-07T14:30:36.719340+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164236719_m2.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
0:00
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"0:00","depth":1,"on_screen":true,"role_description":"text"}]...
|
-3392317590201726397
|
-3392317590201726397
|
click
|
hybrid
|
NULL
|
0:00
ActivityFllesLateJiminny….v# engineering# gen 0:00
ActivityFllesLateJiminny….v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-ofhce# support# thank-vous# the people of jimi.ó- Direct messages(3) Aneliya Angelova. ...8 Stovan Tanev €Stefka Stoyanova1 VesP. Galya Dimitrova. Aneliva Angelova6. Vasil Vasilev¿ James Graham8. Nikolay Ivanov• Lukas Kovali..#:: Apps-T lira Cloud• Toastaesion nanos onWrite a message.sr releases9 22Messagesr Files• BookmarksTodayView JobCircleCl APP 4:32 PMNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-209(https://jiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-205(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fix-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fix-deleting-old-trackso) uimi,nnvlann Added bv GitHublCircled App 5-29 pMI• Deployment Successful!Prolect: appWhen: 05/07/2026 14:29-40lagiView JobMessage wreleasesMay 2026 Week1907:0010:00JDaily - Platform 093Draa to record a part of the screen. Press LW to select a window.15:00 alisunnort DhilvialOpus 4.7 AdaptiveAlways inspect policyName on 429 to knoihack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch endnoints. tin to 100 records ne1(17:30)Thu 7stoyan tomov (Pl0"& days)'Sunnort Dailv 15.Lukas/Stefka 121етка 121Week vTodaySun1100% 12PInu / May 1/.0-a Search eventsLukas/Stetka 121Useful shortcutscontrol * KAlexieva kideGet Calendar to go...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4821
|
173
|
32
|
2026-05-07T14:30:36.838652+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164236838_m1.jpg...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
True
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
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...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Lukas/Stefka 121","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false}]...
|
-1515564132911898322
|
721145986454672002
|
click
|
hybrid
|
NULL
|
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
FirefoxFileEditViewHistoryBookmarksProfiles→ToolsWindowHelp(ah|meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% C8Thu 7 May 17:30:371387826*(GIF)Record gRecord VideoTFTSLukas Kovalik5:30 PMLukas/Stefka 121Lộ3...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4822
|
174
|
23
|
2026-05-07T14:30:38.473148+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164238473_m2.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
WindowActivityMoreJiminny... v# engineering# gener WindowActivityMoreJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovali..:::ADOS-T lira Cloud• Toast# releases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled App5-20 DMO rclerment SuccessfullProlect: apoWhen: 05/07/2026 14:29•40lag.View JobMessage #releasesAarou ve now conmncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueiaesion nanos onMay 2026 Week 19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)Stovan Tanev (PTO - 4 days))Tue 5WedoThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmom07:0010:00 Daily - Platform 09:Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne113:0015:00 aliSunnort Dailv isrod16:0017:00(17:3020:00|Sunnort Daily 15:001Lukas/Stefka 121Sunnort Daily 15:001Opus 4.7 AdaptiveClaude ic Aland can make mictakec Plosce double-chork racnonco.SatLukas sterka 121 • now100% L2• Inu / May 1/.30.40Week v-a Search eventsSun 10Lukas/Stetka 12117:30-18:00 30minJoin Google MeetUseful shortcutsMenu bar calendarcontrol* KTogale sidebarGo to dateAll keyboara snortcutsAlexieva kidk14:00-15:00Get Calendar to go...
|
NULL
|
8251313342300676794
|
NULL
|
click
|
ocr
|
NULL
|
WindowActivityMoreJiminny... v# engineering# gener WindowActivityMoreJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-office# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovafal Ves. Galya Dimitrova. Aneliva Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovali..:::ADOS-T lira Cloud• Toast# releases8 22Messagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)New commits deployed to Prophet Prod-EU:(74673da)(https://github.com/jiminny/prophet/commit/74673da5893290f0116af75beb652b3e4b3dce10) - JY-20:(https://iiminny.atlassian.net/browse/JY-205680): Relax action items assignee (#502) (steliyan-g)GitHiub App 5:02 PM3 new commits pushed to master by mihailmihaylovjiminnybeb8e387 - JY-20817: Fix deleting old tracks8f177131 - Merge branch 'master' into JY-20817-fx-deleting-old-tracks12295204 - Merge pull request #12052 from jiminny/JY-20817-fx-deleting-old-tracksefiminny/app Added bv Gittinheircled App5-20 DMO rclerment SuccessfullProlect: apoWhen: 05/07/2026 14:29•40lag.View JobMessage #releasesAarou ve now conmncu uc asyuuncuy cmoricauy. 1na s uc ouncauon vourmuueiaesion nanos onMay 2026 Week 19Chloe Cross (Parental Leave - 256 days)Andrea Llatanova (Parental Leave - Tos days)Stovan Tanev (PTO - 4 days))Tue 5WedoThu 7Stoyan Tomov (PTO - 2 days)Fri81Den vitazstva nad fasizmom07:0010:00 Daily - Platform 09:Write a message.Always inspect policyName on 429 to knohack offOther operational guidelines• Error responses must stay under 5% ocertificationi• Polling endpoints: minimum interval :• Search query: max 3,000 chars, max 1{results per query.• Ratch enanoints. lin to 100 records ne113:0015:00 aliSunnort Dailv isrod16:0017:00(17:3020:00|Sunnort Daily 15:001Lukas/Stefka 121Sunnort Daily 15:001Opus 4.7 AdaptiveClaude ic Aland can make mictakec Plosce double-chork racnonco.SatLukas sterka 121 • now100% L2• Inu / May 1/.30.40Week v-a Search eventsSun 10Lukas/Stetka 12117:30-18:00 30minJoin Google MeetUseful shortcutsMenu bar calendarcontrol* KTogale sidebarGo to dateAll keyboara snortcutsAlexieva kidk14:00-15:00Get Calendar to go...
|
4820
|
NULL
|
NULL
|
NULL
|
|
4823
|
173
|
33
|
2026-05-07T14:30:42.072584+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164242072_m1.jpg...
|
iTerm2
|
NULL
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Hidden Bar→1(alolmeet.google.com/axk-zwsm-vok?auth Hidden Bar→1(alolmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100%8 • Thu 7 May 17:30:42Lukas Kovalik5:30 PMLukas/Stefka 121...
|
NULL
|
8159659604295345716
|
NULL
|
visual_change
|
ocr
|
NULL
|
Hidden Bar→1(alolmeet.google.com/axk-zwsm-vok?auth Hidden Bar→1(alolmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100%8 • Thu 7 May 17:30:42Lukas Kovalik5:30 PMLukas/Stefka 121...
|
4821
|
NULL
|
NULL
|
NULL
|
|
4824
|
173
|
34
|
2026-05-07T14:30:47.136627+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164247136_m1.jpg...
|
CleanShot X
|
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
0:09
|
[{"role":"AXStaticText","text& [{"role":"AXStaticText","text":"0:09","depth":1,"bounds":{"left":0.75243056,"top":0.96166664,"width":0.023611112,"height":0.015555556},"on_screen":true,"role_description":"text"}]...
|
5263754071476788108
|
5263754071476788108
|
click
|
hybrid
|
NULL
|
0:09
Hidden Bar→1(alolmeet.google.com/axk-zwsm-vok 0:09
Hidden Bar→1(alolmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100%8 • Thu 7 May 17:30:47Lukas Kovalik5:30 PMLukas/Stefka 121• 0:09...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4825
|
174
|
24
|
2026-05-07T14:30:47.308603+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164247308_m2.jpg...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
True
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
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
1
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Lukas Kovalik
5:30
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
Your camera is off.
Background is no longer replaced...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Lukas/Stefka 121","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.016123671,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.27094415,"top":1.0,"width":0.004986702,"height":-0.051875472},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.27310506,"top":1.0,"width":0.010638298,"height":-0.086193085},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.6953125,"top":1.0,"width":0.019115692,"height":-0.06264961},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":22,"bounds":{"left":0.70861036,"top":1.0,"width":0.0018284575,"height":-0.071029544},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.71708775,"top":1.0,"width":0.011968086,"height":-0.06264961},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.7184175,"top":1.0,"width":0.043550532,"height":-0.071029544},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":15,"bounds":{"left":0.73171544,"top":1.0,"width":0.011968086,"height":-0.06264961},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Gemini","depth":22,"bounds":{"left":0.7330452,"top":1.0,"width":0.013464096,"height":-0.071029544},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5:30","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PM","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Lukas/Stefka 121","depth":12,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas/Stefka 121","depth":15,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on microphone","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on camera","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Share screen","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Send a reaction","depth":12,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on captions","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Raise hand (ctrl + ⌘ + h)","depth":12,"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Leave call","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Meeting details","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat with everyone","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Meeting tools","depth":12,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Your camera is off.","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Background is no longer replaced","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
4318065399841895859
|
7985069554552853408
|
click
|
hybrid
|
NULL
|
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
1
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Lukas Kovalik
5:30
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
Your camera is off.
Background is no longer replaced
WindowActivityMoreJiminny... v# engineering# general# jiminny-bg# platform-tickets# product launches# random# releases# soha-ofhce# support# thank-vous# the people of jimi..ó- Direct messages3 Aneliya Angelova. ...Stovan Tanev €Stefka Stoyanovaal Ves% Galya Dimitrova. Aneliya Angelovaa Vasil VasilevJames Grahame. Nikolay Ivanov• Lukas Kovali..:::ADOS-T lira Cloud• Toast# releasesMessagesr Files• BookmarksTodayvView JobCirclecl App 4.22 pMlNew commits deployed to Prophet Prod-US:(74673dal([URL_WITH_CREDENTIALS] - Plattorm TeSevenShores|Hubspot\ExcepticXService-Desk - Queues - Platform• Jy 20807 check various issues wita Sentry••Pull requests • jiminny/appU Useroilot 1 Ask liminny Report GenJY-20773 fix user pilot tracking of8Jiminny0 Search the CRM - HubSpot docsExolore08MonitorsWrite a message.Always inspect policyName on 4hack offOther operational guidelines• Error responses must stay uicertificationi• Polling endpoints: minimun• Search query: max 3,000 cháresults per query.• Ratch endnoints. 11n to 100 r.0 hhllLukas sterka 121 • now100% 52• Inu / May 1/.30.40minny.sentry.lo/ssues//uu/s00o/cenvironment-proaucrtionotent-orocuction-eudoroect=oz41sxquery=is/sAunresolveddrererrer=issue-streamasont-treeIssues / 0e APP-1EED@ Ask Seer &.D: b2e90aбe(LK participatina14 hours ago JSONJumo to: Hiahliahts/app/Services/Crm/Hubspot/Client.php:94 in Jiminny\Services\Crm\Hubspot\Client::getPaginatedData/app/Services/Crm/Hubspot/Service.php:1212 in Jiminny\Services\Crm\Hubspot\Service::Jiminny\Services\Crm\Hubspot\(closure)Called trom:/vendor/laravel/tramework/Src/Illuminate/Cache/Repository.php:504 1n llluminate\cache\Repository:remember/app/Services/Crm/Hubspot/Service.php:1206 in Jiminny\Services\Crm\Hubspot|Service::matchByNamelapo/Services/crm/CachedCrmServiceDecorator.oho:167in Jiminnv_Services Crm.CachedcrmServiceDecorator.matchBvNamelann/Servicos/Crm/CrmActivitvService.ohn:227 inJiminnv|Servicos\Crm\CrmActivitvServico.findCrmRecordsapp/services/crm/crmactivityservice.php:1391n Jiminny services crm Crmactivityservice:.updateParticipantscrmDatalapo//Services/Crm/CrmActivityService.oho:81 in Jiminny Services Crm CrmActivityService«updateCrmData/app/Jobs/Crm/MatchActivityCrmData.php:107 in Jiminny\Jobs\Crm\MatchActivityCrmData::Jiminny\Jobs\Crm\(closure)Called from:/vendor/laravel/framework/src/Illuminate/Database/Concerns/ManagesTransactions.php:35 in Illuminate\Database\Connection::transaction/app/Jobs/Crm/MatchActivityCrmData.php:87 in Jiminny\Jobs\Crm\MatchActivityCrmData::handleif (Sactivity === null) ‹throw new InvalidArgumentException('(MatchActivityCrmDatal Cannot find activity.'):try {Sconnection->transaction(function () use (Sactivity, ScrmActivityService, SactivityRepository) ‹Log::info('[MatchActivityCrmData] Starting CRM data matching'. ['activity' - Sthic-sactivitvld'remote search''set confiaurationi=>Sthis->fromConfiauration2->aetIdo.'old state'activityRepos1toryObject Jiminny Repositories ActivityRepositoryObiect Mluminate Database MariaDbConnectionObject Jiminny\Services\Crm\CrmActivityServiceCalled from:/vendor/laravel/framework/src/llluminate/Container/RoundMethod.nhn:36 inllluminate\Container\RoundMethod«Illuminate\Container\(closure)Jann/Queue/Worker/Worker.nhn-71inJiminnv\Queue|Worker WorkerunrocecsCalled from:/vendor/laravel/framework/src/llluminate/Queue/Worker.ohv:435 inllluminate\QueuelWorker-runJobGuzzleHtto Excention ClientExcentiony trraco ProviowTaasSSIKIPIN viewedIn AopSimilar IssuesViewMeraed IssuesShow 2 more framesIn Appin AopIn AppIn AppIn AppIn AppShow 14 more framesView Full TraceOpus 4.7 AdaptiveClaude ic Aland can make mictakac Plosce double-chork racnonco....
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4826
|
173
|
35
|
2026-05-07T14:30:50.315887+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164250315_m1.jpg...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
True
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
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
1
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Lukas Kovalik
5:30
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
Your camera is off.
Background is no longer replaced...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Lukas/Stefka 121","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88784724,"top":0.08722222,"width":0.039930556,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":22,"bounds":{"left":0.915625,"top":0.09888889,"width":0.0038194444,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.09888889,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":15,"bounds":{"left":0.9638889,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Gemini","depth":22,"bounds":{"left":0.96666664,"top":0.09888889,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.05590278,"top":0.87166667,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5:30","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.022916667,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PM","depth":12,"bounds":{"left":0.07673611,"top":0.9444444,"width":0.016666668,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Lukas/Stefka 121","depth":12,"bounds":{"left":0.11076389,"top":0.9111111,"width":0.08472222,"height":0.08888888},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas/Stefka 121","depth":15,"bounds":{"left":0.11076389,"top":0.9444444,"width":0.08472222,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":13,"bounds":{"left":0.32118055,"top":0.9288889,"width":0.06111111,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on microphone","depth":13,"bounds":{"left":0.34895834,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"bounds":{"left":0.38784721,"top":0.9288889,"width":0.06111111,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on camera","depth":13,"bounds":{"left":0.415625,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Share screen","depth":12,"bounds":{"left":0.45451388,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Send a reaction","depth":12,"bounds":{"left":0.49895832,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on captions","depth":13,"bounds":{"left":0.5434028,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Raise hand (ctrl + ⌘ + h)","depth":12,"bounds":{"left":0.58784723,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options","depth":12,"bounds":{"left":0.6322917,"top":0.9288889,"width":0.025,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Leave call","depth":12,"bounds":{"left":0.6628472,"top":0.9288889,"width":0.05,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Meeting details","depth":12,"bounds":{"left":0.89166665,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat with everyone","depth":12,"bounds":{"left":0.925,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Meeting tools","depth":12,"bounds":{"left":0.9583333,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Your camera is off.","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Background is no longer replaced","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
4318065399841895859
|
7985069554552853408
|
visual_change
|
hybrid
|
NULL
|
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
1
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Lukas Kovalik
5:30
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
Your camera is off.
Background is no longer replaced
FirefoxFileEdit→ CViewHistoryBookmarksProfilesToolsWindowHelp(alolmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100%8 • Thu 7 May 17:30:50=Lukas Kovalik5:30 PMLukas/Stefka 121• 0:12...
|
4824
|
NULL
|
NULL
|
NULL
|
|
4827
|
173
|
36
|
2026-05-07T14:30:52.983239+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164252983_m1.jpg...
|
Firefox
|
Meet - Lukas/Stefka 121 — Work
|
True
|
meet.google.com/axk-zwsm-vok?authuser=lukas.kovali meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
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
1
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:30
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
Your camera is off.
Background is no longer replaced...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Lukas/Stefka 121","depth":4,"bounds":{"left":0.0,"top":0.072222225,"width":0.033680554,"height":0.045555554},"on_screen":true,"help_text":"","role_description":"tab","subrole":"AXTabButton","is_enabled":true,"is_focused":false,"is_selected":true},{"role":"AXButton","text":"Close tab","depth":5,"bounds":{"left":0.0013888889,"top":0.072222225,"width":0.010416667,"height":0.016666668},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"New Tab","depth":4,"bounds":{"left":0.005902778,"top":0.12,"width":0.022222223,"height":0.035555556},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open Google Gemini (⌃X)","depth":6,"bounds":{"left":0.0,"top":0.7977778,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Tabs from other devices","depth":6,"bounds":{"left":0.0,"top":0.8411111,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open history (⇧⌘H)","depth":6,"bounds":{"left":0.0,"top":0.8794444,"width":0.033680554,"height":0.03888889},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Open bookmarks (⌘B)","depth":6,"bounds":{"left":0.0,"top":0.91833335,"width":0.033680554,"height":0.038333334},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Customize sidebar","depth":6,"bounds":{"left":0.0,"top":0.95666665,"width":0.033680554,"height":0.043333333},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88784724,"top":0.08722222,"width":0.039930556,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":22,"bounds":{"left":0.915625,"top":0.09888889,"width":0.0038194444,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Take notes with Gemini","depth":14,"bounds":{"left":0.93333334,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Take notes with Gemini","depth":17,"bounds":{"left":0.9361111,"top":0.09888889,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":15,"bounds":{"left":0.9638889,"top":0.08722222,"width":0.025,"height":0.04},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Gemini","depth":22,"bounds":{"left":0.96666664,"top":0.09888889,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"bounds":{"left":0.47256944,"top":0.5038889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Backgrounds and effects","depth":13,"bounds":{"left":0.503125,"top":0.5038889,"width":0.030555556,"height":0.04888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Lukas Kovalik","depth":13,"bounds":{"left":0.53368056,"top":0.5061111,"width":0.027777778,"height":0.044444446},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Lukas Kovalik","depth":17,"bounds":{"left":0.05590278,"top":0.87166667,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"5:30","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.022916667,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"PM","depth":12,"bounds":{"left":0.07673611,"top":0.9444444,"width":0.016666668,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Lukas/Stefka 121","depth":12,"bounds":{"left":0.11076389,"top":0.9111111,"width":0.08472222,"height":0.08888888},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Lukas/Stefka 121","depth":15,"bounds":{"left":0.11076389,"top":0.9444444,"width":0.08472222,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Audio settings","depth":13,"bounds":{"left":0.32118055,"top":0.9288889,"width":0.06111111,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on microphone","depth":13,"bounds":{"left":0.34895834,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Video settings","depth":13,"bounds":{"left":0.38784721,"top":0.9288889,"width":0.06111111,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on camera","depth":13,"bounds":{"left":0.415625,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":true,"is_selected":false},{"role":"AXButton","text":"Share screen","depth":12,"bounds":{"left":0.45451388,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Send a reaction","depth":12,"bounds":{"left":0.49895832,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Turn on captions","depth":13,"bounds":{"left":0.5434028,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXCheckBox","text":"Raise hand (ctrl + ⌘ + h)","depth":12,"bounds":{"left":0.58784723,"top":0.9288889,"width":0.03888889,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"toggle button","subrole":"AXToggle","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options","depth":12,"bounds":{"left":0.6322917,"top":0.9288889,"width":0.025,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Leave call","depth":12,"bounds":{"left":0.6628472,"top":0.9288889,"width":0.05,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Meeting details","depth":12,"bounds":{"left":0.89166665,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Chat with everyone","depth":12,"bounds":{"left":0.925,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Meeting tools","depth":12,"bounds":{"left":0.9583333,"top":0.9288889,"width":0.033333335,"height":0.053333335},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Your camera is off.","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Background is no longer replaced","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-5924584462111415756
|
7120396020734284576
|
visual_change
|
hybrid
|
NULL
|
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
1
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
You’re continuously framed
Backgrounds and effects
More options for Lukas Kovalik
Lukas Kovalik
5:30
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
Your camera is off.
Background is no longer replaced
FirefoxFileEditViewHistoryBookmarksProfiles→CToolsWindowHelp(alolmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.com| Lukas/Stefka 121 - now100% <47-8 • Thu 7 May 17:30:53$1lollPhpStormLukas Kovalik5:30 PM | Lukas/Stefka 121Lộ3• 0:15....
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4828
|
173
|
37
|
2026-05-07T14:30:55.793370+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164255793_m1.jpg...
|
iTerm2
|
DEV (-zsh)
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.32ms DONE
cache [PASSWORD_DOTS] 10.62ms DONE
compiled [PASSWORD_DOTS] 3.60ms DONE
events [PASSWORD_DOTS] 2.60ms DONE
routes [PASSWORD_DOTS] 2.72ms DONE
views [PASSWORD_DOTS] 5.95ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
HubSpot\Client\Crm\Deals\ApiException
[429] Client error: `GET [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.49ms DONE
cache [PASSWORD_DOTS] 21.31ms DONE
compiled [PASSWORD_DOTS] 3.11ms DONE
events [PASSWORD_DOTS] 5.05ms DONE
routes [PASSWORD_DOTS] 1.83ms DONE
views [PASSWORD_DOTS] 4.91ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 37.77ms DONE
cache [PASSWORD_DOTS] 58.83ms DONE
compiled [PASSWORD_DOTS] 9.93ms DONE
events [PASSWORD_DOTS] 12.23ms DONE
routes [PASSWORD_DOTS] 5.02ms DONE
views [PASSWORD_DOTS] 21.46ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.01ms DONE
cache [PASSWORD_DOTS] 16.11ms DONE
compiled [PASSWORD_DOTS] 2.91ms DONE
events [PASSWORD_DOTS] 2.27ms DONE
routes [PASSWORD_DOTS] 3.11ms DONE
views [PASSWORD_DOTS] 18.41ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
SevenShores\Hubspot\Exceptions\BadRequest
Client error: `POST [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 55.84ms DONE
cache [PASSWORD_DOTS] 108.68ms DONE
compiled [PASSWORD_DOTS] 22.07ms DONE
events [PASSWORD_DOTS] 25.86ms DONE
routes [PASSWORD_DOTS] 19.91ms DONE
views [PASSWORD_DOTS] 52.25ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-audio:worker-audio_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
TypeError
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83
79▕
80▕ // Update reference parameters
81▕ $total = $state->total;
82▕ $lastRecordId = $state->lastRecordId;
➜ 83▕ }
84▕
85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool
86▕ {
87▕ if ($state->hasReachedSafetyLimit()) {
1 app/Services/Crm/Hubspot/Client.php:195
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\Services\Crm\Hubspot\Client), [], "contact")
2 app/Services/Crm/Hubspot/Client.php:176
Jiminny\Services\Crm\Hubspot\Client::getPaginatedDataGenerator([], "contact")
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.73ms DONE
cache [PASSWORD_DOTS] 19.13ms DONE
compiled [PASSWORD_DOTS] 4.93ms DONE
events [PASSWORD_DOTS] 3.02ms DONE
routes [PASSWORD_DOTS] 5.55ms DONE
views [PASSWORD_DOTS] 6.02ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.47ms DONE
cache [PASSWORD_DOTS] 16.66ms DONE
compiled [PASSWORD_DOTS] 3.26ms DONE
events [PASSWORD_DOTS] 3.99ms DONE
routes [PASSWORD_DOTS] 3.85ms DONE
views [PASSWORD_DOTS] 4.93ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.42ms DONE
cache [PASSWORD_DOTS] 23.70ms DONE
compiled [PASSWORD_DOTS] 3.46ms DONE
events [PASSWORD_DOTS] 4.30ms DONE
routes [PASSWORD_DOTS] 8.35ms DONE
views [PASSWORD_DOTS] 8.00ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.77ms DONE
cache [PASSWORD_DOTS] 25.55ms DONE
compiled [PASSWORD_DOTS] 3.87ms DONE
events [PASSWORD_DOTS] 4.96ms DONE
routes [PASSWORD_DOTS] 2.23ms DONE
views [PASSWORD_DOTS] 7.50ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at https://docs.docker.com/go/debug-cli/
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (-zsh)...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.32ms DONE\n cache ............................................................................................................................... 10.62ms DONE\n compiled ............................................................................................................................. 3.60ms DONE\n events ............................................................................................................................... 2.60ms DONE\n routes ............................................................................................................................... 2.72ms DONE\n views ................................................................................................................................ 5.95ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\n\n HubSpot\\Client\\Crm\\Deals\\ApiException \n\n [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n\n at vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704\n 700▕ $options = $this->createHttpClientOption();\n 701▕ try {\n 702▕ $response = $this->client->send($request, $options);\n 703▕ } catch (RequestException $e) {\n ➜ 704▕ throw new ApiException(\n 705▕ \"[{$e->getCode()}] {$e->getMessage()}\",\n 706▕ (int) $e->getCode(),\n 707▕ $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n 708▕ $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n\n +1 vendor frames \n\n 2 app/Services/Crm/Hubspot/Client.php:212\n HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi::getById(\"374720564\", \"hs_object_id,dealname\", \"companies,contacts\")\n\n 3 app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php:130\n Jiminny\\Services\\Crm\\Hubspot\\Client::getOpportunityById(\"374720564\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.49ms DONE\n cache ............................................................................................................................... 21.31ms DONE\n compiled ............................................................................................................................. 3.11ms DONE\n events ............................................................................................................................... 5.05ms DONE\n routes ............................................................................................................................... 1.83ms DONE\n views ................................................................................................................................ 4.91ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 37.77ms DONE\n cache ............................................................................................................................... 58.83ms DONE\n compiled ............................................................................................................................. 9.93ms DONE\n events .............................................................................................................................. 12.23ms DONE\n routes ............................................................................................................................... 5.02ms DONE\n views ............................................................................................................................... 21.46ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.01ms DONE\n cache ............................................................................................................................... 16.11ms DONE\n compiled ............................................................................................................................. 2.91ms DONE\n events ............................................................................................................................... 2.27ms DONE\n routes ............................................................................................................................... 3.11ms DONE\n views ............................................................................................................................... 18.41ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n SevenShores\\Hubspot\\Exceptions\\BadRequest \n\n Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e0284-5 (truncated...)\n\n at vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24\n 20▕ }\n 21▕ \n 22▕ public static function create(RequestException $guzzleException): self\n 23▕ {\n ➜ 24▕ $e = new static(\n 25▕ static::sanitizeResponseMessage($guzzleException->getMessage()),\n 26▕ $guzzleException->getCode(),\n 27▕ $guzzleException\n 28▕ );\n\n +13 vendor frames \n\n 14 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:163\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:51\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 55.84ms DONE\n cache .............................................................................................................................. 108.68ms DONE\n compiled ............................................................................................................................ 22.07ms DONE\n events .............................................................................................................................. 25.86ms DONE\n routes .............................................................................................................................. 19.91ms DONE\n views ............................................................................................................................... 52.25ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n TypeError \n\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83\n 79▕ \n 80▕ // Update reference parameters\n 81▕ $total = $state->total;\n 82▕ $lastRecordId = $state->lastRecordId;\n ➜ 83▕ }\n 84▕ \n 85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool\n 86▕ {\n 87▕ if ($state->hasReachedSafetyLimit()) {\n\n 1 app/Services/Crm/Hubspot/Client.php:195\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), [], \"contact\")\n\n 2 app/Services/Crm/Hubspot/Client.php:176\n Jiminny\\Services\\Crm\\Hubspot\\Client::getPaginatedDataGenerator([], \"contact\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.73ms DONE\n cache ............................................................................................................................... 19.13ms DONE\n compiled ............................................................................................................................. 4.93ms DONE\n events ............................................................................................................................... 3.02ms DONE\n routes ............................................................................................................................... 5.55ms DONE\n views ................................................................................................................................ 6.02ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.47ms DONE\n cache ............................................................................................................................... 16.66ms DONE\n compiled ............................................................................................................................. 3.26ms DONE\n events ............................................................................................................................... 3.99ms DONE\n routes ............................................................................................................................... 3.85ms DONE\n views ................................................................................................................................ 4.93ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.42ms DONE\n cache ............................................................................................................................... 23.70ms DONE\n compiled ............................................................................................................................. 3.46ms DONE\n events ............................................................................................................................... 4.30ms DONE\n routes ............................................................................................................................... 8.35ms DONE\n views ................................................................................................................................ 8.00ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.77ms DONE\n cache ............................................................................................................................... 25.55ms DONE\n compiled ............................................................................................................................. 3.87ms DONE\n events ............................................................................................................................... 4.96ms DONE\n routes ............................................................................................................................... 2.23ms DONE\n views ................................................................................................................................ 7.50ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $","depth":4,"on_screen":true,"value":"root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.32ms DONE\n cache ............................................................................................................................... 10.62ms DONE\n compiled ............................................................................................................................. 3.60ms DONE\n events ............................................................................................................................... 2.60ms DONE\n routes ............................................................................................................................... 2.72ms DONE\n views ................................................................................................................................ 5.95ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\nSyncing opportunity 25\nSyncing opportunity 50\nSyncing opportunity 75\nSyncing opportunity 100\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nSyncing opportunity 0\n\n HubSpot\\Client\\Crm\\Deals\\ApiException \n\n [429] Client error: `GET https://api.hubapi.com/crm/v3/objects/deals/374720564?properties=hs_object_id%2Cdealname&associations=companies%2Ccontacts&archived=0` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your ten_secondly_rolling limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\" (truncated...)\n\n at vendor/hubspot/api-client/codegen/Crm/Deals/Api/BasicApi.php:704\n 700▕ $options = $this->createHttpClientOption();\n 701▕ try {\n 702▕ $response = $this->client->send($request, $options);\n 703▕ } catch (RequestException $e) {\n ➜ 704▕ throw new ApiException(\n 705▕ \"[{$e->getCode()}] {$e->getMessage()}\",\n 706▕ (int) $e->getCode(),\n 707▕ $e->getResponse() ? $e->getResponse()->getHeaders() : null,\n 708▕ $e->getResponse() ? (string) $e->getResponse()->getBody() : null\n\n +1 vendor frames \n\n 2 app/Services/Crm/Hubspot/Client.php:212\n HubSpot\\Client\\Crm\\Deals\\Api\\BasicApi::getById(\"374720564\", \"hs_object_id,dealname\", \"companies,contacts\")\n\n 3 app/Services/Crm/Hubspot/ServiceTraits/OpportunitySyncTrait.php:130\n Jiminny\\Services\\Crm\\Hubspot\\Client::getOpportunityById(\"374720564\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 10.49ms DONE\n cache ............................................................................................................................... 21.31ms DONE\n compiled ............................................................................................................................. 3.11ms DONE\n events ............................................................................................................................... 5.05ms DONE\n routes ............................................................................................................................... 1.83ms DONE\n views ................................................................................................................................ 4.91ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 37.77ms DONE\n cache ............................................................................................................................... 58.83ms DONE\n compiled ............................................................................................................................. 9.93ms DONE\n events .............................................................................................................................. 12.23ms DONE\n routes ............................................................................................................................... 5.02ms DONE\n views ............................................................................................................................... 21.46ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker:worker_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.01ms DONE\n cache ............................................................................................................................... 16.11ms DONE\n compiled ............................................................................................................................. 2.91ms DONE\n events ............................................................................................................................... 2.27ms DONE\n routes ............................................................................................................................... 3.11ms DONE\n views ............................................................................................................................... 18.41ms DONE\n\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n SevenShores\\Hubspot\\Exceptions\\BadRequest \n\n Client error: `POST https://api.hubapi.com/crm/v3/objects/contact/search` resulted in a `429 Too Many Requests` response:\n{\"status\":\"error\",\"message\":\"You have reached your secondly limit.\",\"errorType\":\"RATE_LIMIT\",\"correlationId\":\"019e0284-5 (truncated...)\n\n at vendor/hubspot/hubspot-php/src/Exceptions/HubspotException.php:24\n 20▕ }\n 21▕ \n 22▕ public static function create(RequestException $guzzleException): self\n 23▕ {\n ➜ 24▕ $e = new static(\n 25▕ static::sanitizeResponseMessage($guzzleException->getMessage()),\n 26▕ $guzzleException->getCode(),\n 27▕ $guzzleException\n 28▕ );\n\n +13 vendor frames \n\n 14 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:163\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:51\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 55.84ms DONE\n cache .............................................................................................................................. 108.68ms DONE\n compiled ............................................................................................................................ 22.07ms DONE\n events .............................................................................................................................. 25.86ms DONE\n routes .............................................................................................................................. 19.91ms DONE\n views ............................................................................................................................... 52.25ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nworker-audio:worker-audio_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n TypeError \n\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83\n 79▕ \n 80▕ // Update reference parameters\n 81▕ $total = $state->total;\n 82▕ $lastRecordId = $state->lastRecordId;\n ➜ 83▕ }\n 84▕ \n 85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool\n 86▕ {\n 87▕ if ($state->hasReachedSafetyLimit()) {\n\n 1 app/Services/Crm/Hubspot/Client.php:195\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), [], \"contact\")\n\n 2 app/Services/Crm/Hubspot/Client.php:176\n Jiminny\\Services\\Crm\\Hubspot\\Client::getPaginatedDataGenerator([], \"contact\")\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.73ms DONE\n cache ............................................................................................................................... 19.13ms DONE\n compiled ............................................................................................................................. 4.93ms DONE\n events ............................................................................................................................... 3.02ms DONE\n routes ............................................................................................................................... 5.55ms DONE\n views ................................................................................................................................ 6.02ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 4.47ms DONE\n cache ............................................................................................................................... 16.66ms DONE\n compiled ............................................................................................................................. 3.26ms DONE\n events ............................................................................................................................... 3.99ms DONE\n routes ............................................................................................................................... 3.85ms DONE\n views ................................................................................................................................ 4.93ms DONE\n\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config .............................................................................................................................. 14.42ms DONE\n cache ............................................................................................................................... 23.70ms DONE\n compiled ............................................................................................................................. 3.46ms DONE\n events ............................................................................................................................... 4.30ms DONE\n routes ............................................................................................................................... 8.35ms DONE\n views ................................................................................................................................ 8.00ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-conferences:worker-conferences_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nMatching contact 3\nMatching contact 4\nMatching contact 5\nMatching contact 6\nMatching contact 7\nMatching contact 8\nMatching contact 9\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\n\n Jiminny\\Exceptions\\RateLimitException \n\n Hubspot returned 429\n\n at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206\n 202▕ 'retry_after' => $retryAfter,\n 203▕ 'reason' => $e->getMessage(),\n 204▕ ]);\n 205▕ \n ➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n 207▕ } else {\n 208▕ throw $e;\n 209▕ }\n 210▕ }\n\n +14 vendor frames \n\n 15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166\n SevenShores\\Hubspot\\Http\\Client::request(\"POST\", \"https://api.hubapi.com/crm/v3/objects/contact/search\", [])\n\n 16 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:52\n Jiminny\\Services\\Crm\\Hubspot\\Pagination\\HubspotPaginationService::executeSearchRequest(Object(Jiminny\\Services\\Crm\\Hubspot\\Client), \"https://api.hubapi.com/crm/v3/objects/contact/search\", [], Object(Jiminny\\Services\\Crm\\Hubspot\\Pagination\\PaginationState))\n\nroot@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all\n\n INFO Clearing cached bootstrap files. \n\n config ............................................................................................................................... 6.77ms DONE\n cache ............................................................................................................................... 25.55ms DONE\n compiled ............................................................................................................................. 3.87ms DONE\n events ............................................................................................................................... 4.96ms DONE\n routes ............................................................................................................................... 2.23ms DONE\n views ................................................................................................................................ 7.50ms DONE\n\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped\njiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped\njiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped\njiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped\njiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped\nworker-analytics:worker-analytics_00: stopped\nworker-crm-update:worker-crm-update_00: stopped\nworker-download:worker-download_00: stopped\nworker-nudges:worker-nudges_00: stopped\nworker-conferences:worker-conferences_00: stopped\njiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped\nworker:worker_00: stopped\nworker-audio:worker-audio_00: stopped\nworker-calendar:worker-calendar_00: stopped\nworker-crm-sync:worker-crm-sync_00: stopped\nworker-emails:worker-emails_00: stopped\nworker-es-update:worker-es-update_00: stopped\nartisan-schedule:artisan-schedule_00: stopped\nartisan-schedule:artisan-schedule_00: started\njiminny-worker-processing-1:jiminny-worker-processing-1_00: started\njiminny-worker-processing-2:jiminny-worker-processing-2_00: started\njiminny-worker-processing-3:jiminny-worker-processing-3_00: started\njiminny-worker-processing-4:jiminny-worker-processing-4_00: started\njiminny-worker-processing-5:jiminny-worker-processing-5_00: started\njiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started\nworker:worker_00: started\nworker-analytics:worker-analytics_00: started\nworker-audio:worker-audio_00: started\nworker-calendar:worker-calendar_00: started\nworker-conferences:worker-conferences_00: started\nworker-crm-sync:worker-crm-sync_00: started\nworker-crm-update:worker-crm-update_00: started\nworker-download:worker-download_00: started\nworker-emails:worker-emails_00: started\nworker-es-update:worker-es-update_00: started\nworker-nudges:worker-nudges_00: started\nroot@docker_lamp_1:/home/jiminny# php artisan jiminny:debug\nMatching contact 0\nMatching contact 1\nMatching contact 2\nroot@docker_lamp_1:/home/jiminny# \nWhat's next:\n Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66\n Learn more at https://docs.docker.com/go/debug-cli/\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $","is_focused":true},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.0,"top":0.05888889,"width":0.16458334,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.004166667,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.16458334,"top":0.05888889,"width":0.16458334,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.16875,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.32916668,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.33333334,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.49340278,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.49756944,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.6576389,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.66180557,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.821875,"top":0.05888889,"width":0.16423611,"height":0.026666667},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.82604164,"top":0.06333333,"width":0.011111111,"height":0.017777778},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.95763886,"top":0.032222223,"width":0.03888889,"height":0.018888889},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"DEV (-zsh)","depth":1,"bounds":{"left":0.47569445,"top":0.033333335,"width":0.052083332,"height":0.017777778},"on_screen":true,"role_description":"text"}]...
|
1871752475729997424
|
4433905549572883204
|
visual_change
|
accessibility
|
NULL
|
root@docker_lamp_1:/home/jiminny# php artisan jimi root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.32ms DONE
cache [PASSWORD_DOTS] 10.62ms DONE
compiled [PASSWORD_DOTS] 3.60ms DONE
events [PASSWORD_DOTS] 2.60ms DONE
routes [PASSWORD_DOTS] 2.72ms DONE
views [PASSWORD_DOTS] 5.95ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-audio:worker-audio_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
Syncing opportunity 25
Syncing opportunity 50
Syncing opportunity 75
Syncing opportunity 100
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Syncing opportunity 0
HubSpot\Client\Crm\Deals\ApiException
[429] Client error: `GET [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 10.49ms DONE
cache [PASSWORD_DOTS] 21.31ms DONE
compiled [PASSWORD_DOTS] 3.11ms DONE
events [PASSWORD_DOTS] 5.05ms DONE
routes [PASSWORD_DOTS] 1.83ms DONE
views [PASSWORD_DOTS] 4.91ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 37.77ms DONE
cache [PASSWORD_DOTS] 58.83ms DONE
compiled [PASSWORD_DOTS] 9.93ms DONE
events [PASSWORD_DOTS] 12.23ms DONE
routes [PASSWORD_DOTS] 5.02ms DONE
views [PASSWORD_DOTS] 21.46ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker:worker_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.01ms DONE
cache [PASSWORD_DOTS] 16.11ms DONE
compiled [PASSWORD_DOTS] 2.91ms DONE
events [PASSWORD_DOTS] 2.27ms DONE
routes [PASSWORD_DOTS] 3.11ms DONE
views [PASSWORD_DOTS] 18.41ms DONE
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
SevenShores\Hubspot\Exceptions\BadRequest
Client error: `POST [URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 55.84ms DONE
cache [PASSWORD_DOTS] 108.68ms DONE
compiled [PASSWORD_DOTS] 22.07ms DONE
events [PASSWORD_DOTS] 25.86ms DONE
routes [PASSWORD_DOTS] 19.91ms DONE
views [PASSWORD_DOTS] 52.25ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
artisan-schedule:artisan-schedule_00: stopped
worker-audio:worker-audio_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
TypeError
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(): Return value must be of type Generator, none returned
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:83
79▕
80▕ // Update reference parameters
81▕ $total = $state->total;
82▕ $lastRecordId = $state->lastRecordId;
➜ 83▕ }
84▕
85▕ private function shouldStopPagination(PaginationState $state, int $teamId): bool
86▕ {
87▕ if ($state->hasReachedSafetyLimit()) {
1 app/Services/Crm/Hubspot/Client.php:195
Jiminny\Services\Crm\Hubspot\Pagination\HubspotPaginationService::getPaginatedDataGenerator(Object(Jiminny\Services\Crm\Hubspot\Client), [], "contact")
2 app/Services/Crm/Hubspot/Client.php:176
Jiminny\Services\Crm\Hubspot\Client::getPaginatedDataGenerator([], "contact")
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.73ms DONE
cache [PASSWORD_DOTS] 19.13ms DONE
compiled [PASSWORD_DOTS] 4.93ms DONE
events [PASSWORD_DOTS] 3.02ms DONE
routes [PASSWORD_DOTS] 5.55ms DONE
views [PASSWORD_DOTS] 6.02ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 4.47ms DONE
cache [PASSWORD_DOTS] 16.66ms DONE
compiled [PASSWORD_DOTS] 3.26ms DONE
events [PASSWORD_DOTS] 3.99ms DONE
routes [PASSWORD_DOTS] 3.85ms DONE
views [PASSWORD_DOTS] 4.93ms DONE
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 14.42ms DONE
cache [PASSWORD_DOTS] 23.70ms DONE
compiled [PASSWORD_DOTS] 3.46ms DONE
events [PASSWORD_DOTS] 4.30ms DONE
routes [PASSWORD_DOTS] 8.35ms DONE
views [PASSWORD_DOTS] 8.00ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
worker-crm-update:worker-crm-update_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-conferences:worker-conferences_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
Matching contact 3
Matching contact 4
Matching contact 5
Matching contact 6
Matching contact 7
Matching contact 8
Matching contact 9
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Jiminny\Exceptions\RateLimitException
Hubspot returned 429
at app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:206
202▕ 'retry_after' => $retryAfter,
203▕ 'reason' => $e->getMessage(),
204▕ ]);
205▕
➜ 206▕ throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);
207▕ } else {
208▕ throw $e;
209▕ }
210▕ }
+14 vendor frames
15 app/Services/Crm/Hubspot/Pagination/HubspotPaginationService.php:166
SevenShores\Hubspot\Http\Client::request("POST", "[URL_WITH_CREDENTIALS] php artisan optimize:clear && supervisorctl restart all
INFO Clearing cached bootstrap files.
config [PASSWORD_DOTS] 6.77ms DONE
cache [PASSWORD_DOTS] 25.55ms DONE
compiled [PASSWORD_DOTS] 3.87ms DONE
events [PASSWORD_DOTS] 4.96ms DONE
routes [PASSWORD_DOTS] 2.23ms DONE
views [PASSWORD_DOTS] 7.50ms DONE
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: stopped
jiminny-worker-processing-2:jiminny-worker-processing-2_00: stopped
jiminny-worker-processing-3:jiminny-worker-processing-3_00: stopped
jiminny-worker-processing-4:jiminny-worker-processing-4_00: stopped
jiminny-worker-processing-5:jiminny-worker-processing-5_00: stopped
worker-analytics:worker-analytics_00: stopped
worker-crm-update:worker-crm-update_00: stopped
worker-download:worker-download_00: stopped
worker-nudges:worker-nudges_00: stopped
worker-conferences:worker-conferences_00: stopped
jiminny-worker-processing-1:jiminny-worker-processing-1_00: stopped
worker:worker_00: stopped
worker-audio:worker-audio_00: stopped
worker-calendar:worker-calendar_00: stopped
worker-crm-sync:worker-crm-sync_00: stopped
worker-emails:worker-emails_00: stopped
worker-es-update:worker-es-update_00: stopped
artisan-schedule:artisan-schedule_00: stopped
artisan-schedule:artisan-schedule_00: started
jiminny-worker-processing-1:jiminny-worker-processing-1_00: started
jiminny-worker-processing-2:jiminny-worker-processing-2_00: started
jiminny-worker-processing-3:jiminny-worker-processing-3_00: started
jiminny-worker-processing-4:jiminny-worker-processing-4_00: started
jiminny-worker-processing-5:jiminny-worker-processing-5_00: started
jiminny-worker-processing-delayed:jiminny-worker-processing-delayed_00: started
worker:worker_00: started
worker-analytics:worker-analytics_00: started
worker-audio:worker-audio_00: started
worker-calendar:worker-calendar_00: started
worker-conferences:worker-conferences_00: started
worker-crm-sync:worker-crm-sync_00: started
worker-crm-update:worker-crm-update_00: started
worker-download:worker-download_00: started
worker-emails:worker-emails_00: started
worker-es-update:worker-es-update_00: started
worker-nudges:worker-nudges_00: started
root@docker_lamp_1:/home/jiminny# php artisan jiminny:debug
Matching contact 0
Matching contact 1
Matching contact 2
root@docker_lamp_1:/home/jiminny#
What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug 007d5da3af66
Learn more at https://docs.docker.com/go/debug-cli/
lukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/app (master) $
DOCKER
Close Tab
DEV (-zsh)
Close Tab
APP (-zsh)
Close Tab
-zsh
Close Tab
screenpipe"
Close Tab
-zsh
Close Tab
⌥⌘1
DEV (-zsh)...
|
4827
|
NULL
|
NULL
|
NULL
|
|
4829
|
174
|
25
|
2026-05-07T14:30:57.361480+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164257361_m2.jpg...
|
iTerm2
|
STAGE (ssh)
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
an owner. skipping...
docker_lamp_1 | Syncing obj an owner. skipping...
docker_lamp_1 | Syncing objects for Dev Zoho CRM client (1ece66c8-feb1-4df1-b321-21607daf4623) since 2026-05-07 13:14:41 (delay: 14s)
docker_lamp_1 | [PASSWORD_DOTS] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-05-07 14:14:19 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:19 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 83.54ms DONE
docker_lamp_1 | 2026-05-07 14:14:20 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:20 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 63.63ms DONE
docker_lamp_1 | 2026-05-07 14:14:22 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:23 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-05-07 14:14:24 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:24 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 16.89ms DONE
docker_lamp_1 | 2026-05-07 14:14:26 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:28 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-05-07 14:14:28 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:28 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 40.26ms DONE
docker_lamp_1 | 2026-05-07 14:14:30 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:30 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 77.98ms DONE
docker_lamp_1 | 2026-05-07 14:14:32 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:36 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 3s DONE
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:15:03 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:06 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:08 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:11 Running ['artisan' mailbox:skip-lists:refresh] . 6s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:18 Running ['artisan' mailbox:batch:process --max-batches=15] 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:22 Running ['artisan' activity:purge-stale] ....... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:24 Running ['artisan' mailbox:text-relay:sync] {
docker_lamp_1 | "error": "invalid_request",
docker_lamp_1 | "error_description": "Invalid impersonation \u0026quot;sub\u0026quot; field: @"
docker_lamp_1 | }
docker_lamp_1 | .... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:28 Running ['artisan' conference:pre-meeting-notification] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:30 Running ['artisan' conference:monitor:start] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:33 Running ['artisan' conference:monitor:end] ..... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:end > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:34 Running ['artisan' jiminny:fix-hubspot-tokens] . 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:fix-hubspot-tokens > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:38 Running ['artisan' conference:pre-meeting-reminder] in background 19.64ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' conference:pre-meeting-reminder > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-805efb160ee8d9da02e60364ace7970eb2b35f31" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:15:38 Running ['artisan' hubspot:journal-poll --start] in background 2.93ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' hubspot:journal-poll --start > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-e26d77f915d2c55fe91ca4148a230e32eaa1865e" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:15:38 Running ['artisan' datadog:report:processing-sla-activities] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' datadog:report:processing-sla-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:41 Running ['artisan' activity:sync --from='2026-05-07 13:59:00' --to='2026-05-07 14:15:00' --skipProviders='ringcentral' --skipProviders='avaya' --skipProviders='telus' --skipProviders='talkdesk'] 🚀 Starting HubSpot journal polling service...
docker_lamp_1 | 6s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:sync --from='2026-05-07 13:59:00' --to='2026-05-07 14:15:00' --skipProviders='ringcentral' --skipProviders='avaya' --skipProviders='telus' --skipProviders='talkdesk' > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:47 Running ['artisan' crm:bullhorn:ping --heartbeat] 2026-05-07 14:15:49 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:15:49 Jiminny\Jobs\Activity\SyncActivity ....... 105.02ms DONE
docker_lamp_1 | 2026-05-07 14:15:49 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 0 social account(s) to be processed ...
docker_lamp_1 |
docker_lamp_1 | Done!
docker_lamp_1 | 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:49 Running ['artisan' nudges:send --silent] 2026-05-07 14:15:49 Jiminny\Notifications\Crm\UserAccountDisconnected RUNNING
docker_lamp_1 | 2026-05-07 14:15:49 Jiminny\Notifications\Crm\UserAccountDisconnected 225.37ms DONE
docker_lamp_1 | 2026-05-07 14:15:50 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-05-07 14:15:50 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:15:51 Jiminny\Jobs\Activity\SyncActivity ....... 632.90ms DONE
docker_lamp_1 | 2026-05-07 14:15:51 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:15:52 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-05-07 14:15:52 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:15:52 Jiminny\Jobs\Activity\SyncActivity ....... 485.99ms DONE
docker_lamp_1 | 2026-05-07 14:15:52 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] RUNNING
docker_lamp_1 | ....... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' nudges:send --silent > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:52 Running ['artisan' jiminny:playlists:normalize-sort] 2026-05-07 14:15:54 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:playlists:normalize-sort > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:16:03 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:05 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:08 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:10 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:12 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:15 Running ['artisan' conference:monitor:count] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:17 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-05-07 12:51:19 (delay: 0s)
docker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)
docker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)
docker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)
docker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...
docker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...
docker_lamp_1 | Dispatched 4 HubSpot sync jobs
docker_lamp_1 | ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:20 Running ['artisan' activity:notify-not-logged] 2026-05-07 14:16:20 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] 1s FAIL
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 131.73ms DONE
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 84.82ms DONE
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 123.16ms DONE
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:23 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 407.87ms FAIL
docker_lamp_1 | 2026-05-07 14:16:23 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:24 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 600.83ms FAIL
docker_lamp_1 | . 5s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:notify-not-logged > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:25 Running ['artisan' activity:status-count] {"canceled":28}...... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:status-count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:28 Running ['artisan' mailbox:sync] Queueing 2 inbox(es) for sync.
docker_lamp_1 | [PASSWORD_DOTS] 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-05-07 14:16:36 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:36 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] 130.04ms DONE
docker_lamp_1 | 2026-05-07 14:16:36 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:36 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] 88.95ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:17:11 Running ['artisan' meeting-bot:schedule-bot] ... 8s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:17:20 Running ['artisan' dialers:monitor-activities] . 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:17:24 Running ['artisan' jiminny:monitor-social-accounts] 5s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:17:30 Running ['artisan' mailbox:skip-lists:refresh] . 6s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:17:36 Running ['artisan' mailbox:batch:process --max-batches=15] 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:17:40 Running ['artisan' mailbox:batch:create] ....... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-05-07 14:17:43 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:17:43 Jiminny\Jobs\Mailbox\CreateBatches ....... 137.19ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:18:05 Running ['artisan' meeting-bot:schedule-bot] ... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:18:08 Running ['artisan' dialers:monitor-activities] . 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:18:13 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:18:15 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:18:18 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:18:20 Running ['artisan' conference:monitor:count] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:18:23 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 22.14ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:18:23 Running ['artisan' crm:autolog-delayed] Dispatched autolog delayed jobs for all applicable teams:
docker_lamp_1 | [PASSWORD_DOTS] 7s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:autolog-delayed > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:19:03 Running ['artisan' meeting-bot:schedule-bot] ... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:19:07 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:19:09 Running ['artisan' jiminny:monitor-social-accounts] 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:19:13 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:19:15 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:20:05 Running ['artisan' meeting-bot:schedule-bot] ... 5s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:10 Running ['artisan' dialers:monitor-activities] . 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:15 Running ['artisan' jiminny:monitor-social-accounts] 6s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:21 Running ['artisan' mailbox:skip-lists:refresh] . 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:25 Running ['artisan' mailbox:batch:process --max-batches=15] 2026-05-07 14:20:29,636 WARN stopped: jiminny-worker-processing-3_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,637 WARN stopped: jiminny-worker-processing-2_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,638 WARN stopped: jiminny-worker-processing-4_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,638 WARN stopped: jiminny-worker-processing-5_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,638 WARN stopped: jiminny-worker-processing-delayed_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,639 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,639 INFO waiting for jiminny-worker-processing-1_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-analytics_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-audio_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-calendar_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-conferences_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-crm-sync_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-crm-update_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-download_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-emails_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-es-update_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-nudges_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 WARN stopped: worker-crm-update_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,641 WARN stopped: worker-analytics_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,641 WARN stopped: worker-download_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,641 WARN stopped: worker-nudges_00 (terminated by SIGTERM)
docker_lamp_1 | run_artisan_schedule: Caught SIGTERM signal!
docker_lamp_1 | 2026-05-07 14:20:29,988 INFO stopped: worker-conferences_00 (exit status 0)
docker_lamp_1 | 2026-05-07 14:20:29,988 INFO stopped: jiminny-worker-processing-1_00 (exit status 0)
docker_lamp_1 | 2026-05-07 14:20:29,988 INFO stopped: worker_00 (exit status 0)
docker_lamp_1 | 2026-05-07 14:20:29,994 INFO stopped: worker-audio_00 (exit status 0)
docker_lamp_1 | 2026-05-07 14:20:30,001 INFO stopped: worker-crm-sync_00 (exit status 0)
docker_lamp_1 | 2026-05-07 14:20:30,015 INFO stopped: worker-emails_00 (exit status 0)
docker_lamp_1 | 2026-05-07 14:20:30,045 INFO stopped: worker-calendar_00 (exit status 0)
docker_lamp_1 |
docker_lamp_1 | Hostname docker_lamp_1 pid 49888 gracefully stopped.
docker_lamp_1 | 2026-05-07 14:20:30,906 INFO stopped: worker-es-update_00 (exit status 0)
docker_lamp_1 | 6s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:31 Running ['artisan' conference:monitor:count] 2026-05-07 14:20:32,430 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | 2026-05-07 14:20:34,440 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | ... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:35 Running ['artisan' activity:purge-stale] 2026-05-07 14:20:37,433 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | ....... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:38 Running ['artisan' mailbox:text-relay:sync] 2026-05-07 14:20:39,479 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | {
docker_lamp_1 | "error": "invalid_request",
docker_lamp_1 | "error_description": "Invalid impersonation \u0026quot;sub\u0026quot; field: @"
docker_lamp_1 | }
docker_lamp_1 | 2026-05-07 14:20:41,492 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | .... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:41 Running ['artisan' conference:pre-meeting-notification] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:43 Running ['artisan' conference:monitor:start] 2026-05-07 14:20:44,236 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:45 Running ['artisan' conference:monitor:end] 2026-05-07 14:20:46,400 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | 2026-05-07 14:20:48,419 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | ..... 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:end > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:49 Running ['artisan' jiminny:fix-hubspot-tokens] 2026-05-07 14:20:50,634 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | 2026-05-07 14:20:52,639 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | . 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:fix-hubspot-tokens > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:53 Running ['artisan' conference:pre-meeting-reminder] in background 3.09ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' conference:pre-meeting-reminder > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-805efb160ee8d9da02e60364ace7970eb2b35f31" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:20:53 Running ['artisan' hubspot:journal-poll --start] in background 1.81ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' hubspot:journal-poll --start > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-e26d77f915d2c55fe91ca4148a230e32eaa1865e" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:20:53 Running ['artisan' jiminny:transcription:retry-failed] 2026-05-07 14:20:55,261 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | No failed transcriptions found.
docker_lamp_1 | 2026-05-07 14:20:57,268 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:transcription:retry-failed > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:57 Running ['artisan' crm:reset-governor] 🚀 Starting HubSpot journal polling service...
docker_lamp_1 | 2026-05-07 14:20:59,311 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | [PASSWORD_DOTS] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:reset-governor > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:59 Running ['artisan' crm:bullhorn:ping --heartbeat] 2026-05-07 14:21:01,909 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | 0 social account(s) to be processed ...
docker_lamp_1 |
docker_lamp_1 | Done!
docker_lamp_1 | 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run after SIGTERM
docker_lamp_1 | 2026-05-07 14:21:02,331 WARN stopped: artisan-schedule_00 (exit status 143)
docker_lamp_1 | 2026-05-07 14:21:02,373 INFO spawned: 'artisan-schedule_00' with pid 53379
docker_lamp_1 | 2026-05-07 14:21:02,390 INFO spawned: 'jiminny-worker-processing-1_00' with pid 53380
docker_lamp_1 | 2026-05-07 14:21:02,401 INFO spawned: 'jiminny-worker-processing-2_00' with pid 53381
docker_lamp_1 | 2026-05-07 14:21:02,406 INFO spawned: 'jiminny-worker-processing-3_00' with pid 53382
docker_lamp_1 | 2026-05-07 14:21:02,413 INFO spawned: 'jiminny-worker-processing-4_00' with pid 53386
docker_lamp_1 | 2026-05-07 14:21:02,422 INFO spawned: 'jiminny-worker-processing-5_00' with pid 53388
docker_lamp_1 | 2026-05-07 14:21:02,425 INFO spawned: 'jiminny-worker-processing-delayed_00' with pid 53390
docker_lamp_1 | 2026-05-07 14:21:02,437 INFO spawned: 'worker_00' with pid 53392
docker_lamp_1 | 2026-05-07 14:21:02,440 INFO spawned: 'worker-analytics_00' with pid 53394
docker_lamp_1 | 2026-05-07 14:21:02,445 INFO spawned: 'worker-audio_00' with pid 53396
docker_lamp_1 | 2026-05-07 14:21:02,448 INFO spawned: 'worker-calendar_00' with pid 53397
docker_lamp_1 | 2026-05-07 14:21:02,452 INFO spawned: 'worker-conferences_00' with pid 53398
docker_lamp_1 | 2026-05-07 14:21:02,454 INFO spawned: 'worker-crm-sync_00' with pid 53399
docker_lamp_1 | 2026-05-07 14:21:02,456 INFO spawned: 'worker-crm-update_00' with pid 53400
docker_lamp_1 | 2026-05-07 14:21:02,463 INFO spawned: 'worker-download_00' with pid 53401
docker_lamp_1 | 2026-05-07 14:21:02,474 INFO spawned: 'worker-emails_00' with pid 53404
docker_lamp_1 | 2026-05-07 14:21:02,507 INFO spawned: 'worker-es-update_00' with pid 53405
docker_lamp_1 | 2026-05-07 14:21:02,514 INFO spawned: 'worker-nudges_00' with pid 53406
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: artisan-schedule_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-1_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-2_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-3_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-4_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-5_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-delayed_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-analytics_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-audio_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-calendar_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-conferences_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-crm-sync_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-crm-update_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-download_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-emails_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-es-update_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-nudges_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:22:03 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:05 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:07 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:11 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:12 Running ['artisan' conference:monitor:count] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:14 Running ['artisan' mailbox:batch:create] ....... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:16 Running ['artisan' activity:sync 'ringcentral' 'avaya' 'telus' 'talkdesk' --from='2026-05-07 14:06:00' --to='2026-05-07 14:22:00'] 2026-05-07 14:22:18 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:22:19 Jiminny\Jobs\Mailbox\CreateBatches ....... 745.06ms DONE
docker_lamp_1 | 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:sync 'ringcentral' 'avaya' 'telus' 'talkdesk' --from='2026-05-07 14:06:00' --to='2026-05-07 14:22:00' > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:20 Running ['artisan' twilio:recover-tracks] ...... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' twilio:recover-tracks > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:21 Running ['artisan' dialers:sync-users] [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:sync-users > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:23 Running ['artisan' datadog:report:failed-processing-states] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' datadog:report:failed-processing-states > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:23:03 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:23:05 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:23:07 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:23:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:23:10 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:23:13 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 8.67ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:23:13 Running ['artisan' crm:integration-app-validate-team-connection] Parameter `teamId` is not provided. Loading all teams using IntegrationApp.
docker_lamp_1 | 1 team connections to be validated.
docker_lamp_1 | 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:integration-app-validate-team-connection > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-05-07 14:23:17 Jiminny\Services\Crm\IntegrationApp\Jobs\ValidateTeamActiveConnectionJob RUNNING
docker_lamp_1 | 2026-05-07 14:23:18 Jiminny\Services\Crm\IntegrationApp\Jobs\ValidateTeamActiveConnectionJob 751.64ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:24:05 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:08 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:11 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:13 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:16 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:18 Running ['artisan' conference:monitor:count] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:20 Running ['artisan' activity:aircall:check-and-renew] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:aircall:check-and-renew > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:22 Running ['artisan' track:retry-failed-downloads] 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' track:retry-failed-downloads > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:25:04 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:06 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:09 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:12 Running ['artisan' mailbox:skip-lists:refresh] . 7s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:20 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:22 Running ['artisan' activity:purge-stale] ....... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:26 Running ['artisan' mailbox:text-relay:sync] {
docker_lamp_1 | "error": "invalid_request",
docker_lamp_1 | "error_description": "Invalid impersonation \u0026quot;sub\u0026quot; field: @"
docker_lamp_1 | }
docker_lamp_1 | .... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:29 Running ['artisan' conference:pre-meeting-notification] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:32 Running ['artisan' conference:monitor:start] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:33 Running ['artisan' conference:monitor:end] ..... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:end > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:36 Running ['artisan' jiminny:fix-hubspot-tokens] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:fix-hubspot-tokens > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:38 Running ['artisan' conference:pre-meeting-reminder] in background 2.53ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' conference:pre-meeting-reminder > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-805efb160ee8d9da02e60364ace7970eb2b35f31" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:25:38 Running ['artisan' hubspot:journal-poll --start] in background 1.58ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' hubspot:journal-poll --start > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-e26d77f915d2c55fe91ca4148a230e32eaa1865e" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:25:38 Running ['artisan' crm:bullhorn:ping --heartbeat] 0 social account(s) to be processed ...
docker_lamp_1 |
docker_lamp_1 | Done!
docker_lamp_1 | 🚀 Starting HubSpot journal polling service...
docker_lamp_1 | 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:26:03 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:05 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:08 Running ['artisan' jiminny:monitor-social-accounts] 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:11 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:14 Running ['artisan' mailbox:batch:process --max-batches=15] 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:17 Running ['artisan' conference:monitor:count] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:19 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-05-07 12:51:19 (delay: 0s)
docker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)
docker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)
docker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)
docker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...
docker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...
docker_lamp_1 | Dispatched 4 HubSpot sync jobs
docker_lamp_1 | ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:22 Running ['artisan' activity:notify-not-logged] 2026-05-07 14:26:23 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:23 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 417.37ms FAIL
docker_lamp_1 | 2026-05-07 14:26:23 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:23 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 55.56ms DONE
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 39.36ms DONE
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 23.82ms DONE
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 265.85ms FAIL
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:notify-not-logged > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:24 Running ['artisan' activity:status-count] 2026-05-07 14:26:25 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 620.43ms FAIL
docker_lamp_1 | {"canceled":28}...... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:status-count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:27 Running ['artisan' mailbox:sync] Queueing 2 inbox(es) for sync.
docker_lamp_1 | [PASSWORD_DOTS] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-05-07 14:26:30 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:30 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] 55.28ms DONE
docker_lamp_1 | 2026-05-07 14:26:30 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:30 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] 51.97ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:27:03 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:27:04 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:27:07 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:27:10 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:27:12 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:27:14 Running ['artisan' mailbox:batch:create] ....... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-05-07 14:27:18 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:27:18 Jiminny\Jobs\Mailbox\CreateBatches ....... 203.80ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:28:04 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:28:06 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:28:09 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:28:11 Running ['artisan' mailbox:skip-lists:refresh] . 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:28:14 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:28:15 Running ['artisan' conference:monitor:count] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:28:18 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 3.59ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:28:18 Running ['artisan' calendar:sync --dateMode=daily] 2026-05-07 14:28:28 Jiminny\Jobs\Calendar\SyncCalendarE...
|
[{"role":"AXTextArea","text [{"role":"AXTextArea","text":"an owner. skipping...\ndocker_lamp_1 | Syncing objects for Dev Zoho CRM client (1ece66c8-feb1-4df1-b321-21607daf4623) since 2026-05-07 13:14:41 (delay: 14s)\ndocker_lamp_1 | ........... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:14:19 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:19 Jiminny\\Jobs\\Crm\\SyncObjects .............. 83.54ms DONE\ndocker_lamp_1 | 2026-05-07 14:14:20 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:20 Jiminny\\Jobs\\Crm\\SyncObjects .............. 63.63ms DONE\ndocker_lamp_1 | 2026-05-07 14:14:22 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:23 Jiminny\\Jobs\\Crm\\SyncObjects ................... 1s DONE\ndocker_lamp_1 | 2026-05-07 14:14:24 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:24 Jiminny\\Jobs\\Crm\\SyncObjects .............. 16.89ms DONE\ndocker_lamp_1 | 2026-05-07 14:14:26 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:28 Jiminny\\Jobs\\Crm\\SyncObjects ................... 1s DONE\ndocker_lamp_1 | 2026-05-07 14:14:28 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:28 Jiminny\\Jobs\\Crm\\SyncObjects .............. 40.26ms DONE\ndocker_lamp_1 | 2026-05-07 14:14:30 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:30 Jiminny\\Jobs\\Crm\\SyncObjects .............. 77.98ms DONE\ndocker_lamp_1 | 2026-05-07 14:14:32 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:36 Jiminny\\Jobs\\Crm\\SyncObjects ................... 3s DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:15:03 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:06 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:08 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:11 Running ['artisan' mailbox:skip-lists:refresh] . 6s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:18 Running ['artisan' mailbox:batch:process --max-batches=15] 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:22 Running ['artisan' activity:purge-stale] ....... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:24 Running ['artisan' mailbox:text-relay:sync] {\ndocker_lamp_1 | \"error\": \"invalid_request\",\ndocker_lamp_1 | \"error_description\": \"Invalid impersonation \\u0026quot;sub\\u0026quot; field: @\"\ndocker_lamp_1 | }\ndocker_lamp_1 | .... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:28 Running ['artisan' conference:pre-meeting-notification] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:30 Running ['artisan' conference:monitor:start] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:33 Running ['artisan' conference:monitor:end] ..... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:end > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:34 Running ['artisan' jiminny:fix-hubspot-tokens] . 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:fix-hubspot-tokens > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:38 Running ['artisan' conference:pre-meeting-reminder] in background 19.64ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' conference:pre-meeting-reminder > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-805efb160ee8d9da02e60364ace7970eb2b35f31\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:15:38 Running ['artisan' hubspot:journal-poll --start] in background 2.93ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' hubspot:journal-poll --start > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-e26d77f915d2c55fe91ca4148a230e32eaa1865e\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:15:38 Running ['artisan' datadog:report:processing-sla-activities] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' datadog:report:processing-sla-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:41 Running ['artisan' activity:sync --from='2026-05-07 13:59:00' --to='2026-05-07 14:15:00' --skipProviders='ringcentral' --skipProviders='avaya' --skipProviders='telus' --skipProviders='talkdesk'] 🚀\u0000 Starting HubSpot journal polling service...\ndocker_lamp_1 | 6s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:sync --from='2026-05-07 13:59:00' --to='2026-05-07 14:15:00' --skipProviders='ringcentral' --skipProviders='avaya' --skipProviders='telus' --skipProviders='talkdesk' > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:47 Running ['artisan' crm:bullhorn:ping --heartbeat] 2026-05-07 14:15:49 Jiminny\\Jobs\\Activity\\SyncActivity ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:15:49 Jiminny\\Jobs\\Activity\\SyncActivity ....... 105.02ms DONE\ndocker_lamp_1 | 2026-05-07 14:15:49 Jiminny\\Jobs\\Activity\\SyncActivity ............. RUNNING\ndocker_lamp_1 | 0 social account(s) to be processed ...\ndocker_lamp_1 | \ndocker_lamp_1 | Done!\ndocker_lamp_1 | 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:49 Running ['artisan' nudges:send --silent] 2026-05-07 14:15:49 Jiminny\\Notifications\\Crm\\UserAccountDisconnected RUNNING\ndocker_lamp_1 | 2026-05-07 14:15:49 Jiminny\\Notifications\\Crm\\UserAccountDisconnected 225.37ms DONE\ndocker_lamp_1 | 2026-05-07 14:15:50 Jiminny\\Jobs\\Activity\\SyncActivity ............. 1s DONE\ndocker_lamp_1 | 2026-05-07 14:15:50 Jiminny\\Jobs\\Activity\\SyncActivity ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:15:51 Jiminny\\Jobs\\Activity\\SyncActivity ....... 632.90ms DONE\ndocker_lamp_1 | 2026-05-07 14:15:51 Jiminny\\Jobs\\Activity\\SyncActivity ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:15:52 Jiminny\\Jobs\\Activity\\SyncActivity ............. 1s DONE\ndocker_lamp_1 | 2026-05-07 14:15:52 Jiminny\\Jobs\\Activity\\SyncActivity ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:15:52 Jiminny\\Jobs\\Activity\\SyncActivity ....... 485.99ms DONE\ndocker_lamp_1 | 2026-05-07 14:15:52 Jiminny\\Jobs\\Activity\\SyncActivity ............. RUNNING\ndocker_lamp_1 | ....... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' nudges:send --silent > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:52 Running ['artisan' jiminny:playlists:normalize-sort] 2026-05-07 14:15:54 Jiminny\\Jobs\\Activity\\SyncActivity ............. 1s DONE\ndocker_lamp_1 | 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:playlists:normalize-sort > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:16:03 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:05 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:08 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:10 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:12 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:15 Running ['artisan' conference:monitor:count] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:17 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-05-07 12:51:19 (delay: 0s)\ndocker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)\ndocker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)\ndocker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)\ndocker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Dispatched 4 HubSpot sync jobs\ndocker_lamp_1 | ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:20 Running ['artisan' activity:notify-not-logged] 2026-05-07 14:16:20 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ 1s FAIL\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 131.73ms DONE\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 84.82ms DONE\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 123.16ms DONE\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:23 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 407.87ms FAIL\ndocker_lamp_1 | 2026-05-07 14:16:23 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 600.83ms FAIL\ndocker_lamp_1 | . 5s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:notify-not-logged > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:25 Running ['artisan' activity:status-count] {\"canceled\":28}...... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:status-count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:28 Running ['artisan' mailbox:sync] Queueing 2 inbox(es) for sync.\ndocker_lamp_1 | ............... 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:16:36 Jiminny\\Jobs\\Mailbox\\SyncInbox ................. RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:36 Jiminny\\Jobs\\Mailbox\\SyncInbox ........... 130.04ms DONE\ndocker_lamp_1 | 2026-05-07 14:16:36 Jiminny\\Jobs\\Mailbox\\SyncInbox ................. RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:36 Jiminny\\Jobs\\Mailbox\\SyncInbox ............ 88.95ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:17:11 Running ['artisan' meeting-bot:schedule-bot] ... 8s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:17:20 Running ['artisan' dialers:monitor-activities] . 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:17:24 Running ['artisan' jiminny:monitor-social-accounts] 5s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:17:30 Running ['artisan' mailbox:skip-lists:refresh] . 6s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:17:36 Running ['artisan' mailbox:batch:process --max-batches=15] 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:17:40 Running ['artisan' mailbox:batch:create] ....... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:17:43 Jiminny\\Jobs\\Mailbox\\CreateBatches ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:17:43 Jiminny\\Jobs\\Mailbox\\CreateBatches ....... 137.19ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:18:05 Running ['artisan' meeting-bot:schedule-bot] ... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:18:08 Running ['artisan' dialers:monitor-activities] . 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:18:13 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:18:15 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:18:18 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:18:20 Running ['artisan' conference:monitor:count] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:18:23 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 22.14ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:18:23 Running ['artisan' crm:autolog-delayed] Dispatched autolog delayed jobs for all applicable teams: \ndocker_lamp_1 | ........ 7s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:autolog-delayed > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:19:03 Running ['artisan' meeting-bot:schedule-bot] ... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:19:07 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:19:09 Running ['artisan' jiminny:monitor-social-accounts] 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:19:13 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:19:15 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:20:05 Running ['artisan' meeting-bot:schedule-bot] ... 5s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:10 Running ['artisan' dialers:monitor-activities] . 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:15 Running ['artisan' jiminny:monitor-social-accounts] 6s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:21 Running ['artisan' mailbox:skip-lists:refresh] . 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:25 Running ['artisan' mailbox:batch:process --max-batches=15] 2026-05-07 14:20:29,636 WARN stopped: jiminny-worker-processing-3_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,637 WARN stopped: jiminny-worker-processing-2_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,638 WARN stopped: jiminny-worker-processing-4_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,638 WARN stopped: jiminny-worker-processing-5_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,638 WARN stopped: jiminny-worker-processing-delayed_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,639 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,639 INFO waiting for jiminny-worker-processing-1_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-analytics_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-audio_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-calendar_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-conferences_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-crm-sync_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-crm-update_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-download_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-emails_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-es-update_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-nudges_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 WARN stopped: worker-crm-update_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,641 WARN stopped: worker-analytics_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,641 WARN stopped: worker-download_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,641 WARN stopped: worker-nudges_00 (terminated by SIGTERM)\ndocker_lamp_1 | run_artisan_schedule: Caught SIGTERM signal!\ndocker_lamp_1 | 2026-05-07 14:20:29,988 INFO stopped: worker-conferences_00 (exit status 0)\ndocker_lamp_1 | 2026-05-07 14:20:29,988 INFO stopped: jiminny-worker-processing-1_00 (exit status 0)\ndocker_lamp_1 | 2026-05-07 14:20:29,988 INFO stopped: worker_00 (exit status 0)\ndocker_lamp_1 | 2026-05-07 14:20:29,994 INFO stopped: worker-audio_00 (exit status 0)\ndocker_lamp_1 | 2026-05-07 14:20:30,001 INFO stopped: worker-crm-sync_00 (exit status 0)\ndocker_lamp_1 | 2026-05-07 14:20:30,015 INFO stopped: worker-emails_00 (exit status 0)\ndocker_lamp_1 | 2026-05-07 14:20:30,045 INFO stopped: worker-calendar_00 (exit status 0)\ndocker_lamp_1 | \ndocker_lamp_1 | Hostname docker_lamp_1 pid 49888 gracefully stopped.\ndocker_lamp_1 | 2026-05-07 14:20:30,906 INFO stopped: worker-es-update_00 (exit status 0)\ndocker_lamp_1 | 6s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:31 Running ['artisan' conference:monitor:count] 2026-05-07 14:20:32,430 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:34,440 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | ... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:35 Running ['artisan' activity:purge-stale] 2026-05-07 14:20:37,433 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | ....... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:38 Running ['artisan' mailbox:text-relay:sync] 2026-05-07 14:20:39,479 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | {\ndocker_lamp_1 | \"error\": \"invalid_request\",\ndocker_lamp_1 | \"error_description\": \"Invalid impersonation \\u0026quot;sub\\u0026quot; field: @\"\ndocker_lamp_1 | }\ndocker_lamp_1 | 2026-05-07 14:20:41,492 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | .... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:41 Running ['artisan' conference:pre-meeting-notification] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:43 Running ['artisan' conference:monitor:start] 2026-05-07 14:20:44,236 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:45 Running ['artisan' conference:monitor:end] 2026-05-07 14:20:46,400 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:48,419 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | ..... 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:end > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:49 Running ['artisan' jiminny:fix-hubspot-tokens] 2026-05-07 14:20:50,634 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:52,639 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | . 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:fix-hubspot-tokens > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:53 Running ['artisan' conference:pre-meeting-reminder] in background 3.09ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' conference:pre-meeting-reminder > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-805efb160ee8d9da02e60364ace7970eb2b35f31\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:20:53 Running ['artisan' hubspot:journal-poll --start] in background 1.81ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' hubspot:journal-poll --start > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-e26d77f915d2c55fe91ca4148a230e32eaa1865e\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:20:53 Running ['artisan' jiminny:transcription:retry-failed] 2026-05-07 14:20:55,261 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | No failed transcriptions found.\ndocker_lamp_1 | 2026-05-07 14:20:57,268 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:transcription:retry-failed > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:57 Running ['artisan' crm:reset-governor] 🚀\u0000 Starting HubSpot journal polling service...\ndocker_lamp_1 | 2026-05-07 14:20:59,311 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | ......... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:reset-governor > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:59 Running ['artisan' crm:bullhorn:ping --heartbeat] 2026-05-07 14:21:01,909 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | 0 social account(s) to be processed ...\ndocker_lamp_1 | \ndocker_lamp_1 | Done!\ndocker_lamp_1 | 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run after SIGTERM\ndocker_lamp_1 | 2026-05-07 14:21:02,331 WARN stopped: artisan-schedule_00 (exit status 143)\ndocker_lamp_1 | 2026-05-07 14:21:02,373 INFO spawned: 'artisan-schedule_00' with pid 53379\ndocker_lamp_1 | 2026-05-07 14:21:02,390 INFO spawned: 'jiminny-worker-processing-1_00' with pid 53380\ndocker_lamp_1 | 2026-05-07 14:21:02,401 INFO spawned: 'jiminny-worker-processing-2_00' with pid 53381\ndocker_lamp_1 | 2026-05-07 14:21:02,406 INFO spawned: 'jiminny-worker-processing-3_00' with pid 53382\ndocker_lamp_1 | 2026-05-07 14:21:02,413 INFO spawned: 'jiminny-worker-processing-4_00' with pid 53386\ndocker_lamp_1 | 2026-05-07 14:21:02,422 INFO spawned: 'jiminny-worker-processing-5_00' with pid 53388\ndocker_lamp_1 | 2026-05-07 14:21:02,425 INFO spawned: 'jiminny-worker-processing-delayed_00' with pid 53390\ndocker_lamp_1 | 2026-05-07 14:21:02,437 INFO spawned: 'worker_00' with pid 53392\ndocker_lamp_1 | 2026-05-07 14:21:02,440 INFO spawned: 'worker-analytics_00' with pid 53394\ndocker_lamp_1 | 2026-05-07 14:21:02,445 INFO spawned: 'worker-audio_00' with pid 53396\ndocker_lamp_1 | 2026-05-07 14:21:02,448 INFO spawned: 'worker-calendar_00' with pid 53397\ndocker_lamp_1 | 2026-05-07 14:21:02,452 INFO spawned: 'worker-conferences_00' with pid 53398\ndocker_lamp_1 | 2026-05-07 14:21:02,454 INFO spawned: 'worker-crm-sync_00' with pid 53399\ndocker_lamp_1 | 2026-05-07 14:21:02,456 INFO spawned: 'worker-crm-update_00' with pid 53400\ndocker_lamp_1 | 2026-05-07 14:21:02,463 INFO spawned: 'worker-download_00' with pid 53401\ndocker_lamp_1 | 2026-05-07 14:21:02,474 INFO spawned: 'worker-emails_00' with pid 53404\ndocker_lamp_1 | 2026-05-07 14:21:02,507 INFO spawned: 'worker-es-update_00' with pid 53405\ndocker_lamp_1 | 2026-05-07 14:21:02,514 INFO spawned: 'worker-nudges_00' with pid 53406\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: artisan-schedule_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-1_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-2_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-3_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-4_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-5_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-delayed_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-analytics_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-audio_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-calendar_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-conferences_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-crm-sync_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-crm-update_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-download_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-emails_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-es-update_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-nudges_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:22:03 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:05 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:07 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:11 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:12 Running ['artisan' conference:monitor:count] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:14 Running ['artisan' mailbox:batch:create] ....... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:16 Running ['artisan' activity:sync 'ringcentral' 'avaya' 'telus' 'talkdesk' --from='2026-05-07 14:06:00' --to='2026-05-07 14:22:00'] 2026-05-07 14:22:18 Jiminny\\Jobs\\Mailbox\\CreateBatches ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:22:19 Jiminny\\Jobs\\Mailbox\\CreateBatches ....... 745.06ms DONE\ndocker_lamp_1 | 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:sync 'ringcentral' 'avaya' 'telus' 'talkdesk' --from='2026-05-07 14:06:00' --to='2026-05-07 14:22:00' > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:20 Running ['artisan' twilio:recover-tracks] ...... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' twilio:recover-tracks > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:21 Running ['artisan' dialers:sync-users] ......... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:sync-users > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:23 Running ['artisan' datadog:report:failed-processing-states] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' datadog:report:failed-processing-states > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:23:03 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:23:05 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:23:07 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:23:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:23:10 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:23:13 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 8.67ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:23:13 Running ['artisan' crm:integration-app-validate-team-connection] Parameter `teamId` is not provided. Loading all teams using IntegrationApp.\ndocker_lamp_1 | 1 team connections to be validated.\ndocker_lamp_1 | 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:integration-app-validate-team-connection > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:23:17 Jiminny\\Services\\Crm\\IntegrationApp\\Jobs\\ValidateTeamActiveConnectionJob RUNNING\ndocker_lamp_1 | 2026-05-07 14:23:18 Jiminny\\Services\\Crm\\IntegrationApp\\Jobs\\ValidateTeamActiveConnectionJob 751.64ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:24:05 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:08 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:11 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:13 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:16 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:18 Running ['artisan' conference:monitor:count] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:20 Running ['artisan' activity:aircall:check-and-renew] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:aircall:check-and-renew > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:22 Running ['artisan' track:retry-failed-downloads] 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' track:retry-failed-downloads > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:25:04 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:06 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:09 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:12 Running ['artisan' mailbox:skip-lists:refresh] . 7s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:20 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:22 Running ['artisan' activity:purge-stale] ....... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:26 Running ['artisan' mailbox:text-relay:sync] {\ndocker_lamp_1 | \"error\": \"invalid_request\",\ndocker_lamp_1 | \"error_description\": \"Invalid impersonation \\u0026quot;sub\\u0026quot; field: @\"\ndocker_lamp_1 | }\ndocker_lamp_1 | .... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:29 Running ['artisan' conference:pre-meeting-notification] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:32 Running ['artisan' conference:monitor:start] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:33 Running ['artisan' conference:monitor:end] ..... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:end > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:36 Running ['artisan' jiminny:fix-hubspot-tokens] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:fix-hubspot-tokens > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:38 Running ['artisan' conference:pre-meeting-reminder] in background 2.53ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' conference:pre-meeting-reminder > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-805efb160ee8d9da02e60364ace7970eb2b35f31\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:25:38 Running ['artisan' hubspot:journal-poll --start] in background 1.58ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' hubspot:journal-poll --start > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-e26d77f915d2c55fe91ca4148a230e32eaa1865e\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:25:38 Running ['artisan' crm:bullhorn:ping --heartbeat] 0 social account(s) to be processed ...\ndocker_lamp_1 | \ndocker_lamp_1 | Done!\ndocker_lamp_1 | 🚀\u0000 Starting HubSpot journal polling service...\ndocker_lamp_1 | 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:26:03 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:05 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:08 Running ['artisan' jiminny:monitor-social-accounts] 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:11 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:14 Running ['artisan' mailbox:batch:process --max-batches=15] 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:17 Running ['artisan' conference:monitor:count] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:19 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-05-07 12:51:19 (delay: 0s)\ndocker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)\ndocker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)\ndocker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)\ndocker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Dispatched 4 HubSpot sync jobs\ndocker_lamp_1 | ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:22 Running ['artisan' activity:notify-not-logged] 2026-05-07 14:26:23 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:23 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 417.37ms FAIL\ndocker_lamp_1 | 2026-05-07 14:26:23 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:23 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 55.56ms DONE\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 39.36ms DONE\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 23.82ms DONE\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 265.85ms FAIL\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:notify-not-logged > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:24 Running ['artisan' activity:status-count] 2026-05-07 14:26:25 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 620.43ms FAIL\ndocker_lamp_1 | {\"canceled\":28}...... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:status-count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:27 Running ['artisan' mailbox:sync] Queueing 2 inbox(es) for sync.\ndocker_lamp_1 | ............... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:26:30 Jiminny\\Jobs\\Mailbox\\SyncInbox ................. RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:30 Jiminny\\Jobs\\Mailbox\\SyncInbox ............ 55.28ms DONE\ndocker_lamp_1 | 2026-05-07 14:26:30 Jiminny\\Jobs\\Mailbox\\SyncInbox ................. RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:30 Jiminny\\Jobs\\Mailbox\\SyncInbox ............ 51.97ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:27:03 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:27:04 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:27:07 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:27:10 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:27:12 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:27:14 Running ['artisan' mailbox:batch:create] ....... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:27:18 Jiminny\\Jobs\\Mailbox\\CreateBatches ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:27:18 Jiminny\\Jobs\\Mailbox\\CreateBatches ....... 203.80ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:28:04 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:28:06 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:28:09 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:28:11 Running ['artisan' mailbox:skip-lists:refresh] . 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:28:14 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:28:15 Running ['artisan' conference:monitor:count] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:28:18 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 3.59ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:28:18 Running ['artisan' calendar:sync --dateMode=daily] 2026-05-07 14:28:28 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-05-07 14:28:29 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... 1s DONE\ndocker_lamp_1 | 13s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' calendar:sync --dateMode=daily > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:28:33 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-05-07 14:28:34 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents . 645.34ms DONE\ndocker_lamp_1 | 2026-05-07 14:28:34 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-05-07 14:28:34 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents . 123.73ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:29:14 Running ['artisan' meeting-bot:schedule-bot] .. 11s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:29:26 Running ['artisan' dialers:monitor-activities] . 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:29:30 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:29:33 Running ['artisan' mailbox:skip-lists:refresh] . 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:29:37 Running ['artisan' mailbox:batch:process --max-batches=15] 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:30:06 Running ['artisan' meeting-bot:schedule-bot] ... 6s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:13 Running ['artisan' dialers:monitor-activities] . 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:17 Running ['artisan' jiminny:monitor-social-accounts] 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:20 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:23 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:25 Running ['artisan' conference:monitor:count] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:27 Running ['artisan' activity:purge-stale] ....... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:30 Running ['artisan' mailbox:text-relay:sync] {\ndocker_lamp_1 | \"error\": \"invalid_request\",\ndocker_lamp_1 | \"error_description\": \"Invalid impersonation \\u0026quot;sub\\u0026quot; field: @\"\ndocker_lamp_1 | }\ndocker_lamp_1 | .... 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:35 Running ['artisan' conference:pre-meeting-notification] 10s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1 \nunexpected EOF\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $","depth":4,"on_screen":true,"value":"an owner. skipping...\ndocker_lamp_1 | Syncing objects for Dev Zoho CRM client (1ece66c8-feb1-4df1-b321-21607daf4623) since 2026-05-07 13:14:41 (delay: 14s)\ndocker_lamp_1 | ........... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:14:19 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:19 Jiminny\\Jobs\\Crm\\SyncObjects .............. 83.54ms DONE\ndocker_lamp_1 | 2026-05-07 14:14:20 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:20 Jiminny\\Jobs\\Crm\\SyncObjects .............. 63.63ms DONE\ndocker_lamp_1 | 2026-05-07 14:14:22 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:23 Jiminny\\Jobs\\Crm\\SyncObjects ................... 1s DONE\ndocker_lamp_1 | 2026-05-07 14:14:24 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:24 Jiminny\\Jobs\\Crm\\SyncObjects .............. 16.89ms DONE\ndocker_lamp_1 | 2026-05-07 14:14:26 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:28 Jiminny\\Jobs\\Crm\\SyncObjects ................... 1s DONE\ndocker_lamp_1 | 2026-05-07 14:14:28 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:28 Jiminny\\Jobs\\Crm\\SyncObjects .............. 40.26ms DONE\ndocker_lamp_1 | 2026-05-07 14:14:30 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:30 Jiminny\\Jobs\\Crm\\SyncObjects .............. 77.98ms DONE\ndocker_lamp_1 | 2026-05-07 14:14:32 Jiminny\\Jobs\\Crm\\SyncObjects ................... RUNNING\ndocker_lamp_1 | 2026-05-07 14:14:36 Jiminny\\Jobs\\Crm\\SyncObjects ................... 3s DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:15:03 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:06 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:08 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:11 Running ['artisan' mailbox:skip-lists:refresh] . 6s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:18 Running ['artisan' mailbox:batch:process --max-batches=15] 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:22 Running ['artisan' activity:purge-stale] ....... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:24 Running ['artisan' mailbox:text-relay:sync] {\ndocker_lamp_1 | \"error\": \"invalid_request\",\ndocker_lamp_1 | \"error_description\": \"Invalid impersonation \\u0026quot;sub\\u0026quot; field: @\"\ndocker_lamp_1 | }\ndocker_lamp_1 | .... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:28 Running ['artisan' conference:pre-meeting-notification] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:30 Running ['artisan' conference:monitor:start] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:33 Running ['artisan' conference:monitor:end] ..... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:end > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:34 Running ['artisan' jiminny:fix-hubspot-tokens] . 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:fix-hubspot-tokens > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:38 Running ['artisan' conference:pre-meeting-reminder] in background 19.64ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' conference:pre-meeting-reminder > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-805efb160ee8d9da02e60364ace7970eb2b35f31\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:15:38 Running ['artisan' hubspot:journal-poll --start] in background 2.93ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' hubspot:journal-poll --start > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-e26d77f915d2c55fe91ca4148a230e32eaa1865e\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:15:38 Running ['artisan' datadog:report:processing-sla-activities] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' datadog:report:processing-sla-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:41 Running ['artisan' activity:sync --from='2026-05-07 13:59:00' --to='2026-05-07 14:15:00' --skipProviders='ringcentral' --skipProviders='avaya' --skipProviders='telus' --skipProviders='talkdesk'] 🚀\u0000 Starting HubSpot journal polling service...\ndocker_lamp_1 | 6s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:sync --from='2026-05-07 13:59:00' --to='2026-05-07 14:15:00' --skipProviders='ringcentral' --skipProviders='avaya' --skipProviders='telus' --skipProviders='talkdesk' > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:47 Running ['artisan' crm:bullhorn:ping --heartbeat] 2026-05-07 14:15:49 Jiminny\\Jobs\\Activity\\SyncActivity ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:15:49 Jiminny\\Jobs\\Activity\\SyncActivity ....... 105.02ms DONE\ndocker_lamp_1 | 2026-05-07 14:15:49 Jiminny\\Jobs\\Activity\\SyncActivity ............. RUNNING\ndocker_lamp_1 | 0 social account(s) to be processed ...\ndocker_lamp_1 | \ndocker_lamp_1 | Done!\ndocker_lamp_1 | 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:49 Running ['artisan' nudges:send --silent] 2026-05-07 14:15:49 Jiminny\\Notifications\\Crm\\UserAccountDisconnected RUNNING\ndocker_lamp_1 | 2026-05-07 14:15:49 Jiminny\\Notifications\\Crm\\UserAccountDisconnected 225.37ms DONE\ndocker_lamp_1 | 2026-05-07 14:15:50 Jiminny\\Jobs\\Activity\\SyncActivity ............. 1s DONE\ndocker_lamp_1 | 2026-05-07 14:15:50 Jiminny\\Jobs\\Activity\\SyncActivity ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:15:51 Jiminny\\Jobs\\Activity\\SyncActivity ....... 632.90ms DONE\ndocker_lamp_1 | 2026-05-07 14:15:51 Jiminny\\Jobs\\Activity\\SyncActivity ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:15:52 Jiminny\\Jobs\\Activity\\SyncActivity ............. 1s DONE\ndocker_lamp_1 | 2026-05-07 14:15:52 Jiminny\\Jobs\\Activity\\SyncActivity ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:15:52 Jiminny\\Jobs\\Activity\\SyncActivity ....... 485.99ms DONE\ndocker_lamp_1 | 2026-05-07 14:15:52 Jiminny\\Jobs\\Activity\\SyncActivity ............. RUNNING\ndocker_lamp_1 | ....... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' nudges:send --silent > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:15:52 Running ['artisan' jiminny:playlists:normalize-sort] 2026-05-07 14:15:54 Jiminny\\Jobs\\Activity\\SyncActivity ............. 1s DONE\ndocker_lamp_1 | 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:playlists:normalize-sort > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:16:03 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:05 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:08 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:10 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:12 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:15 Running ['artisan' conference:monitor:count] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:17 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-05-07 12:51:19 (delay: 0s)\ndocker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)\ndocker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)\ndocker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)\ndocker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Dispatched 4 HubSpot sync jobs\ndocker_lamp_1 | ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:20 Running ['artisan' activity:notify-not-logged] 2026-05-07 14:16:20 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ 1s FAIL\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 131.73ms DONE\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 84.82ms DONE\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 123.16ms DONE\ndocker_lamp_1 | 2026-05-07 14:16:22 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:23 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 407.87ms FAIL\ndocker_lamp_1 | 2026-05-07 14:16:23 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 600.83ms FAIL\ndocker_lamp_1 | . 5s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:notify-not-logged > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:25 Running ['artisan' activity:status-count] {\"canceled\":28}...... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:status-count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:16:28 Running ['artisan' mailbox:sync] Queueing 2 inbox(es) for sync.\ndocker_lamp_1 | ............... 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:16:36 Jiminny\\Jobs\\Mailbox\\SyncInbox ................. RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:36 Jiminny\\Jobs\\Mailbox\\SyncInbox ........... 130.04ms DONE\ndocker_lamp_1 | 2026-05-07 14:16:36 Jiminny\\Jobs\\Mailbox\\SyncInbox ................. RUNNING\ndocker_lamp_1 | 2026-05-07 14:16:36 Jiminny\\Jobs\\Mailbox\\SyncInbox ............ 88.95ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:17:11 Running ['artisan' meeting-bot:schedule-bot] ... 8s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:17:20 Running ['artisan' dialers:monitor-activities] . 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:17:24 Running ['artisan' jiminny:monitor-social-accounts] 5s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:17:30 Running ['artisan' mailbox:skip-lists:refresh] . 6s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:17:36 Running ['artisan' mailbox:batch:process --max-batches=15] 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:17:40 Running ['artisan' mailbox:batch:create] ....... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:17:43 Jiminny\\Jobs\\Mailbox\\CreateBatches ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:17:43 Jiminny\\Jobs\\Mailbox\\CreateBatches ....... 137.19ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:18:05 Running ['artisan' meeting-bot:schedule-bot] ... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:18:08 Running ['artisan' dialers:monitor-activities] . 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:18:13 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:18:15 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:18:18 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:18:20 Running ['artisan' conference:monitor:count] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:18:23 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 22.14ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:18:23 Running ['artisan' crm:autolog-delayed] Dispatched autolog delayed jobs for all applicable teams: \ndocker_lamp_1 | ........ 7s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:autolog-delayed > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:19:03 Running ['artisan' meeting-bot:schedule-bot] ... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:19:07 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:19:09 Running ['artisan' jiminny:monitor-social-accounts] 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:19:13 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:19:15 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:20:05 Running ['artisan' meeting-bot:schedule-bot] ... 5s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:10 Running ['artisan' dialers:monitor-activities] . 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:15 Running ['artisan' jiminny:monitor-social-accounts] 6s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:21 Running ['artisan' mailbox:skip-lists:refresh] . 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:25 Running ['artisan' mailbox:batch:process --max-batches=15] 2026-05-07 14:20:29,636 WARN stopped: jiminny-worker-processing-3_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,637 WARN stopped: jiminny-worker-processing-2_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,638 WARN stopped: jiminny-worker-processing-4_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,638 WARN stopped: jiminny-worker-processing-5_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,638 WARN stopped: jiminny-worker-processing-delayed_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,639 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,639 INFO waiting for jiminny-worker-processing-1_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-analytics_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-audio_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-calendar_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-conferences_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-crm-sync_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-crm-update_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-download_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-emails_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-es-update_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-nudges_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:29,640 WARN stopped: worker-crm-update_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,641 WARN stopped: worker-analytics_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,641 WARN stopped: worker-download_00 (terminated by SIGTERM)\ndocker_lamp_1 | 2026-05-07 14:20:29,641 WARN stopped: worker-nudges_00 (terminated by SIGTERM)\ndocker_lamp_1 | run_artisan_schedule: Caught SIGTERM signal!\ndocker_lamp_1 | 2026-05-07 14:20:29,988 INFO stopped: worker-conferences_00 (exit status 0)\ndocker_lamp_1 | 2026-05-07 14:20:29,988 INFO stopped: jiminny-worker-processing-1_00 (exit status 0)\ndocker_lamp_1 | 2026-05-07 14:20:29,988 INFO stopped: worker_00 (exit status 0)\ndocker_lamp_1 | 2026-05-07 14:20:29,994 INFO stopped: worker-audio_00 (exit status 0)\ndocker_lamp_1 | 2026-05-07 14:20:30,001 INFO stopped: worker-crm-sync_00 (exit status 0)\ndocker_lamp_1 | 2026-05-07 14:20:30,015 INFO stopped: worker-emails_00 (exit status 0)\ndocker_lamp_1 | 2026-05-07 14:20:30,045 INFO stopped: worker-calendar_00 (exit status 0)\ndocker_lamp_1 | \ndocker_lamp_1 | Hostname docker_lamp_1 pid 49888 gracefully stopped.\ndocker_lamp_1 | 2026-05-07 14:20:30,906 INFO stopped: worker-es-update_00 (exit status 0)\ndocker_lamp_1 | 6s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:31 Running ['artisan' conference:monitor:count] 2026-05-07 14:20:32,430 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:34,440 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | ... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:35 Running ['artisan' activity:purge-stale] 2026-05-07 14:20:37,433 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | ....... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:38 Running ['artisan' mailbox:text-relay:sync] 2026-05-07 14:20:39,479 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | {\ndocker_lamp_1 | \"error\": \"invalid_request\",\ndocker_lamp_1 | \"error_description\": \"Invalid impersonation \\u0026quot;sub\\u0026quot; field: @\"\ndocker_lamp_1 | }\ndocker_lamp_1 | 2026-05-07 14:20:41,492 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | .... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:41 Running ['artisan' conference:pre-meeting-notification] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:43 Running ['artisan' conference:monitor:start] 2026-05-07 14:20:44,236 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:45 Running ['artisan' conference:monitor:end] 2026-05-07 14:20:46,400 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:48,419 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | ..... 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:end > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:49 Running ['artisan' jiminny:fix-hubspot-tokens] 2026-05-07 14:20:50,634 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | 2026-05-07 14:20:52,639 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | . 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:fix-hubspot-tokens > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:53 Running ['artisan' conference:pre-meeting-reminder] in background 3.09ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' conference:pre-meeting-reminder > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-805efb160ee8d9da02e60364ace7970eb2b35f31\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:20:53 Running ['artisan' hubspot:journal-poll --start] in background 1.81ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' hubspot:journal-poll --start > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-e26d77f915d2c55fe91ca4148a230e32eaa1865e\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:20:53 Running ['artisan' jiminny:transcription:retry-failed] 2026-05-07 14:20:55,261 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | No failed transcriptions found.\ndocker_lamp_1 | 2026-05-07 14:20:57,268 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:transcription:retry-failed > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:57 Running ['artisan' crm:reset-governor] 🚀\u0000 Starting HubSpot journal polling service...\ndocker_lamp_1 | 2026-05-07 14:20:59,311 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | ......... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:reset-governor > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:20:59 Running ['artisan' crm:bullhorn:ping --heartbeat] 2026-05-07 14:21:01,909 INFO waiting for artisan-schedule_00 to stop\ndocker_lamp_1 | 0 social account(s) to be processed ...\ndocker_lamp_1 | \ndocker_lamp_1 | Done!\ndocker_lamp_1 | 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run after SIGTERM\ndocker_lamp_1 | 2026-05-07 14:21:02,331 WARN stopped: artisan-schedule_00 (exit status 143)\ndocker_lamp_1 | 2026-05-07 14:21:02,373 INFO spawned: 'artisan-schedule_00' with pid 53379\ndocker_lamp_1 | 2026-05-07 14:21:02,390 INFO spawned: 'jiminny-worker-processing-1_00' with pid 53380\ndocker_lamp_1 | 2026-05-07 14:21:02,401 INFO spawned: 'jiminny-worker-processing-2_00' with pid 53381\ndocker_lamp_1 | 2026-05-07 14:21:02,406 INFO spawned: 'jiminny-worker-processing-3_00' with pid 53382\ndocker_lamp_1 | 2026-05-07 14:21:02,413 INFO spawned: 'jiminny-worker-processing-4_00' with pid 53386\ndocker_lamp_1 | 2026-05-07 14:21:02,422 INFO spawned: 'jiminny-worker-processing-5_00' with pid 53388\ndocker_lamp_1 | 2026-05-07 14:21:02,425 INFO spawned: 'jiminny-worker-processing-delayed_00' with pid 53390\ndocker_lamp_1 | 2026-05-07 14:21:02,437 INFO spawned: 'worker_00' with pid 53392\ndocker_lamp_1 | 2026-05-07 14:21:02,440 INFO spawned: 'worker-analytics_00' with pid 53394\ndocker_lamp_1 | 2026-05-07 14:21:02,445 INFO spawned: 'worker-audio_00' with pid 53396\ndocker_lamp_1 | 2026-05-07 14:21:02,448 INFO spawned: 'worker-calendar_00' with pid 53397\ndocker_lamp_1 | 2026-05-07 14:21:02,452 INFO spawned: 'worker-conferences_00' with pid 53398\ndocker_lamp_1 | 2026-05-07 14:21:02,454 INFO spawned: 'worker-crm-sync_00' with pid 53399\ndocker_lamp_1 | 2026-05-07 14:21:02,456 INFO spawned: 'worker-crm-update_00' with pid 53400\ndocker_lamp_1 | 2026-05-07 14:21:02,463 INFO spawned: 'worker-download_00' with pid 53401\ndocker_lamp_1 | 2026-05-07 14:21:02,474 INFO spawned: 'worker-emails_00' with pid 53404\ndocker_lamp_1 | 2026-05-07 14:21:02,507 INFO spawned: 'worker-es-update_00' with pid 53405\ndocker_lamp_1 | 2026-05-07 14:21:02,514 INFO spawned: 'worker-nudges_00' with pid 53406\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: artisan-schedule_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-1_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-2_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-3_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-4_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-5_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-delayed_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-analytics_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-audio_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-calendar_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-conferences_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-crm-sync_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-crm-update_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-download_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-emails_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-es-update_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-nudges_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:22:03 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:05 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:07 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:11 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:12 Running ['artisan' conference:monitor:count] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:14 Running ['artisan' mailbox:batch:create] ....... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:16 Running ['artisan' activity:sync 'ringcentral' 'avaya' 'telus' 'talkdesk' --from='2026-05-07 14:06:00' --to='2026-05-07 14:22:00'] 2026-05-07 14:22:18 Jiminny\\Jobs\\Mailbox\\CreateBatches ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:22:19 Jiminny\\Jobs\\Mailbox\\CreateBatches ....... 745.06ms DONE\ndocker_lamp_1 | 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:sync 'ringcentral' 'avaya' 'telus' 'talkdesk' --from='2026-05-07 14:06:00' --to='2026-05-07 14:22:00' > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:20 Running ['artisan' twilio:recover-tracks] ...... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' twilio:recover-tracks > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:21 Running ['artisan' dialers:sync-users] ......... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:sync-users > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:22:23 Running ['artisan' datadog:report:failed-processing-states] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' datadog:report:failed-processing-states > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:23:03 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:23:05 Running ['artisan' dialers:monitor-activities] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:23:07 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:23:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:23:10 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:23:13 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 8.67ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:23:13 Running ['artisan' crm:integration-app-validate-team-connection] Parameter `teamId` is not provided. Loading all teams using IntegrationApp.\ndocker_lamp_1 | 1 team connections to be validated.\ndocker_lamp_1 | 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:integration-app-validate-team-connection > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:23:17 Jiminny\\Services\\Crm\\IntegrationApp\\Jobs\\ValidateTeamActiveConnectionJob RUNNING\ndocker_lamp_1 | 2026-05-07 14:23:18 Jiminny\\Services\\Crm\\IntegrationApp\\Jobs\\ValidateTeamActiveConnectionJob 751.64ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:24:05 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:08 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:11 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:13 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:16 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:18 Running ['artisan' conference:monitor:count] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:20 Running ['artisan' activity:aircall:check-and-renew] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:aircall:check-and-renew > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:24:22 Running ['artisan' track:retry-failed-downloads] 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' track:retry-failed-downloads > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:25:04 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:06 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:09 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:12 Running ['artisan' mailbox:skip-lists:refresh] . 7s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:20 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:22 Running ['artisan' activity:purge-stale] ....... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:26 Running ['artisan' mailbox:text-relay:sync] {\ndocker_lamp_1 | \"error\": \"invalid_request\",\ndocker_lamp_1 | \"error_description\": \"Invalid impersonation \\u0026quot;sub\\u0026quot; field: @\"\ndocker_lamp_1 | }\ndocker_lamp_1 | .... 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:29 Running ['artisan' conference:pre-meeting-notification] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:32 Running ['artisan' conference:monitor:start] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:33 Running ['artisan' conference:monitor:end] ..... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:end > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:36 Running ['artisan' jiminny:fix-hubspot-tokens] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:fix-hubspot-tokens > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:25:38 Running ['artisan' conference:pre-meeting-reminder] in background 2.53ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' conference:pre-meeting-reminder > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-805efb160ee8d9da02e60364ace7970eb2b35f31\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:25:38 Running ['artisan' hubspot:journal-poll --start] in background 1.58ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' hubspot:journal-poll --start > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-e26d77f915d2c55fe91ca4148a230e32eaa1865e\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:25:38 Running ['artisan' crm:bullhorn:ping --heartbeat] 0 social account(s) to be processed ...\ndocker_lamp_1 | \ndocker_lamp_1 | Done!\ndocker_lamp_1 | 🚀\u0000 Starting HubSpot journal polling service...\ndocker_lamp_1 | 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:26:03 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:05 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:08 Running ['artisan' jiminny:monitor-social-accounts] 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:11 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:14 Running ['artisan' mailbox:batch:process --max-batches=15] 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:17 Running ['artisan' conference:monitor:count] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:19 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-05-07 12:51:19 (delay: 0s)\ndocker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)\ndocker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)\ndocker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)\ndocker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...\ndocker_lamp_1 | Dispatched 4 HubSpot sync jobs\ndocker_lamp_1 | ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:22 Running ['artisan' activity:notify-not-logged] 2026-05-07 14:26:23 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:23 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 417.37ms FAIL\ndocker_lamp_1 | 2026-05-07 14:26:23 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:23 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 55.56ms DONE\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 39.36ms DONE\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ....... 23.82ms DONE\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 265.85ms FAIL\ndocker_lamp_1 | 2026-05-07 14:26:24 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ............ RUNNING\ndocker_lamp_1 | . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:notify-not-logged > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:24 Running ['artisan' activity:status-count] 2026-05-07 14:26:25 Jiminny\\Jobs\\Crm\\SyncHubspotObjects ...... 620.43ms FAIL\ndocker_lamp_1 | {\"canceled\":28}...... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:status-count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:26:27 Running ['artisan' mailbox:sync] Queueing 2 inbox(es) for sync.\ndocker_lamp_1 | ............... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:26:30 Jiminny\\Jobs\\Mailbox\\SyncInbox ................. RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:30 Jiminny\\Jobs\\Mailbox\\SyncInbox ............ 55.28ms DONE\ndocker_lamp_1 | 2026-05-07 14:26:30 Jiminny\\Jobs\\Mailbox\\SyncInbox ................. RUNNING\ndocker_lamp_1 | 2026-05-07 14:26:30 Jiminny\\Jobs\\Mailbox\\SyncInbox ............ 51.97ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:27:03 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:27:04 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:27:07 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:27:10 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:27:12 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:27:14 Running ['artisan' mailbox:batch:create] ....... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:27:18 Jiminny\\Jobs\\Mailbox\\CreateBatches ............. RUNNING\ndocker_lamp_1 | 2026-05-07 14:27:18 Jiminny\\Jobs\\Mailbox\\CreateBatches ....... 203.80ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:28:04 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:28:06 Running ['artisan' dialers:monitor-activities] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:28:09 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:28:11 Running ['artisan' mailbox:skip-lists:refresh] . 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:28:14 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:28:15 Running ['artisan' conference:monitor:count] ... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:28:18 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 3.59ms DONE\ndocker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish \"framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11\" \"$?\") > '/dev/null' 2>&1 & \ndocker_lamp_1 | 2026-05-07 14:28:18 Running ['artisan' calendar:sync --dateMode=daily] 2026-05-07 14:28:28 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-05-07 14:28:29 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... 1s DONE\ndocker_lamp_1 | 13s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' calendar:sync --dateMode=daily > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | 2026-05-07 14:28:33 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-05-07 14:28:34 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents . 645.34ms DONE\ndocker_lamp_1 | 2026-05-07 14:28:34 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents ....... RUNNING\ndocker_lamp_1 | 2026-05-07 14:28:34 Jiminny\\Jobs\\Calendar\\SyncCalendarEvents . 123.73ms DONE\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:29:14 Running ['artisan' meeting-bot:schedule-bot] .. 11s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:29:26 Running ['artisan' dialers:monitor-activities] . 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:29:30 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:29:33 Running ['artisan' mailbox:skip-lists:refresh] . 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:29:37 Running ['artisan' mailbox:batch:process --max-batches=15] 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | \ndocker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run\ndocker_lamp_1 | \ndocker_lamp_1 | 2026-05-07 14:30:06 Running ['artisan' meeting-bot:schedule-bot] ... 6s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:13 Running ['artisan' dialers:monitor-activities] . 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:17 Running ['artisan' jiminny:monitor-social-accounts] 3s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:20 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:23 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:25 Running ['artisan' conference:monitor:count] ... 1s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:27 Running ['artisan' activity:purge-stale] ....... 2s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:30 Running ['artisan' mailbox:text-relay:sync] {\ndocker_lamp_1 | \"error\": \"invalid_request\",\ndocker_lamp_1 | \"error_description\": \"Invalid impersonation \\u0026quot;sub\\u0026quot; field: @\"\ndocker_lamp_1 | }\ndocker_lamp_1 | .... 4s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1 \ndocker_lamp_1 | 2026-05-07 14:30:35 Running ['artisan' conference:pre-meeting-notification] 10s DONE\ndocker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1 \nunexpected EOF\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~/jiminny/infrastructure/dev/docker (develop) $","is_focused":true},{"role":"AXButton","text":"Menu","depth":3,"bounds":{"left":0.50299203,"top":1.0,"width":0.004986702,"height":-0.06424582},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥1 DOCKER (-zsh)","depth":3,"bounds":{"left":0.27925533,"top":1.0,"width":0.22207446,"height":-0.06464481},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Last login: Thu May 7 09:29:14 on console\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Thu May 7 08:01:12 UTC 2026\n\n System load: 0.0 Processes: 144\n Usage of /: 58.4% of 7.57GB Users logged in: 5\n Memory usage: 37% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n38 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Mon Apr 27 07:45:27 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$","depth":5,"bounds":{"left":0.5103058,"top":0.59217876,"width":0.2400266,"height":0.40782124},"on_screen":true,"lines":[{"char_start":0,"char_count":43,"bounds":{"left":0.5119681,"top":0.59217876,"width":0.1143617,"height":0.014365523}},{"char_start":43,"char_count":1,"bounds":{"left":0.5119681,"top":0.6065443,"width":0.0026595744,"height":0.014365523}},{"char_start":44,"char_count":75,"bounds":{"left":0.5119681,"top":0.6209098,"width":0.19946809,"height":0.014365523}},{"char_start":119,"char_count":1,"bounds":{"left":0.5119681,"top":0.63527536,"width":0.0026595744,"height":0.014365523}},{"char_start":120,"char_count":75,"bounds":{"left":0.5119681,"top":0.64964086,"width":0.19946809,"height":0.014365523}},{"char_start":195,"char_count":50,"bounds":{"left":0.5119681,"top":0.6640064,"width":0.13297872,"height":0.014365523}},{"char_start":245,"char_count":49,"bounds":{"left":0.5119681,"top":0.6783719,"width":0.13031915,"height":0.014365523}},{"char_start":294,"char_count":64,"bounds":{"left":0.5119681,"top":0.6927374,"width":0.17021276,"height":0.014365523}},{"char_start":358,"char_count":1,"bounds":{"left":0.5119681,"top":0.70710295,"width":0.0026595744,"height":0.014365523}},{"char_start":359,"char_count":43,"bounds":{"left":0.5119681,"top":0.72146845,"width":0.1143617,"height":0.014365523}},{"char_start":402,"char_count":51,"bounds":{"left":0.5119681,"top":0.735834,"width":0.1356383,"height":0.014365523}},{"char_start":453,"char_count":42,"bounds":{"left":0.5119681,"top":0.7501995,"width":0.11170213,"height":0.014365523}},{"char_start":495,"char_count":1,"bounds":{"left":0.5119681,"top":0.76456505,"width":0.0026595744,"height":0.014365523}},{"char_start":496,"char_count":55,"bounds":{"left":0.5119681,"top":0.77893054,"width":0.1462766,"height":0.014365523}},{"char_start":551,"char_count":1,"bounds":{"left":0.5119681,"top":0.7932961,"width":0.0026595744,"height":0.014365523}},{"char_start":552,"char_count":61,"bounds":{"left":0.5119681,"top":0.8076616,"width":0.16223404,"height":0.014365523}},{"char_start":613,"char_count":59,"bounds":{"left":0.5119681,"top":0.82202715,"width":0.15691489,"height":0.014365523}}],"value":"Last login: Thu May 7 09:29:14 on console\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ prod\n(lukas@jiminny-prod-bastion) Verification code: \nWelcome to Ubuntu 22.04.5 LTS (GNU/Linux 6.8.0-1041-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Thu May 7 08:01:12 UTC 2026\n\n System load: 0.0 Processes: 144\n Usage of /: 58.4% of 7.57GB Users logged in: 5\n Memory usage: 37% IPv4 address for eth0: 10.30.45.167\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n38 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Mon Apr 27 07:45:27 2026 from 212.5.153.87\nlukas@jiminny-prod-bastion:~$","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"bounds":{"left":0.74335104,"top":1.0,"width":0.004986702,"height":-0.06424582},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥2 PROD (ssh)","depth":4,"bounds":{"left":0.5192819,"top":1.0,"width":0.22240691,"height":-0.06464481},"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Last login: Thu May 7 09:29:14 on console\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"on_screen":true,"value":"Last login: Thu May 7 09:29:14 on console\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥3 EU (-zsh)","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Last login: Thu May 7 09:29:14 on console\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ stg\n(lukas@jiminny-stage-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1052-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Thu May 7 11:01:47 UTC 2026\n\n System load: 0.0 Processes: 120\n Usage of /: 56.9% of 7.57GB Users logged in: 2\n Memory usage: 33% IPv4 address for ens5: 10.30.46.154\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n82 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 28 06:25:10 2026 from 212.5.153.87\nlukas@jiminny-stage-bastion:~$","depth":5,"bounds":{"left":0.5103058,"top":0.82202715,"width":0.2400266,"height":0.17797285},"on_screen":true,"value":"Last login: Thu May 7 09:29:14 on console\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $ stg\n(lukas@jiminny-stage-bastion) Verification code: \nWelcome to Ubuntu 22.04.4 LTS (GNU/Linux 6.8.0-1052-aws x86_64)\n\n * Documentation: https://help.ubuntu.com\n * Management: https://landscape.canonical.com\n * Support: https://ubuntu.com/pro\n\n System information as of Thu May 7 11:01:47 UTC 2026\n\n System load: 0.0 Processes: 120\n Usage of /: 56.9% of 7.57GB Users logged in: 2\n Memory usage: 33% IPv4 address for ens5: 10.30.46.154\n Swap usage: 0%\n\n * Ubuntu Pro delivers the most comprehensive open source security and\n compliance features.\n\n https://ubuntu.com/aws/pro\n\nExpanded Security Maintenance for Applications is not enabled.\n\n82 updates can be applied immediately.\nTo see these additional updates run: apt list --upgradable\n\nEnable ESM Apps to receive additional future security updates.\nSee https://ubuntu.com/esm or run: sudo pro status\n\nNew release '24.04.4 LTS' available.\nRun 'do-release-upgrade' to upgrade to it.\n\n\n*** System restart required ***\nLast login: Tue Apr 28 06:25:10 2026 from 212.5.153.87\nlukas@jiminny-stage-bastion:~$","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥4 STAGE (ssh)","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys002\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys002\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥5 QA (-zsh)","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys004\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys004\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥6 FE (-zsh)","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXTextArea","text":"Last login: Thu May 7 09:44:56 on ttys004\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","depth":5,"on_screen":true,"value":"Last login: Thu May 7 09:44:56 on ttys004\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\n\nPoetry could not find a pyproject.toml file in /Users/lukas or its parents\nlukas@Lukas-Kovaliks-MacBook-Pro-Jiminny ~ $","is_focused":true},{"role":"AXButton","text":"Menu","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false},{"role":"AXStaticText","text":"⌥7 EXT (-zsh)","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXRadioButton","text":"DOCKER","depth":2,"bounds":{"left":0.27027926,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.27227393,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"DEV (-zsh)","depth":2,"bounds":{"left":0.34906915,"top":1.0,"width":0.0787899,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.35106382,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"APP (-zsh)","depth":2,"bounds":{"left":0.42785904,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.42985374,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.5064827,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.5084774,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"screenpipe\"","depth":2,"bounds":{"left":0.5851064,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.58710104,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXRadioButton","text":"-zsh","depth":2,"bounds":{"left":0.66373,"top":1.0,"width":0.07862367,"height":-0.042298436},"on_screen":true,"role_description":"radio button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close Tab","depth":3,"bounds":{"left":0.66572475,"top":1.0,"width":0.005319149,"height":-0.04549086},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"⌥⌘1","depth":1,"bounds":{"left":0.7287234,"top":1.0,"width":0.01861702,"height":-0.023144484},"on_screen":true,"automation_id":"_NS:8","role_description":"text"},{"role":"AXStaticText","text":"STAGE (ssh)","depth":1,"bounds":{"left":0.49634308,"top":1.0,"width":0.027925532,"height":-0.02394259},"on_screen":true,"role_description":"text"}]...
|
-5349801653500152072
|
-4793128393062954047
|
click
|
accessibility
|
NULL
|
an owner. skipping...
docker_lamp_1 | Syncing obj an owner. skipping...
docker_lamp_1 | Syncing objects for Dev Zoho CRM client (1ece66c8-feb1-4df1-b321-21607daf4623) since 2026-05-07 13:14:41 (delay: 14s)
docker_lamp_1 | [PASSWORD_DOTS] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-05-07 14:14:19 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:19 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 83.54ms DONE
docker_lamp_1 | 2026-05-07 14:14:20 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:20 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 63.63ms DONE
docker_lamp_1 | 2026-05-07 14:14:22 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:23 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-05-07 14:14:24 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:24 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 16.89ms DONE
docker_lamp_1 | 2026-05-07 14:14:26 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:28 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-05-07 14:14:28 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:28 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 40.26ms DONE
docker_lamp_1 | 2026-05-07 14:14:30 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:30 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 77.98ms DONE
docker_lamp_1 | 2026-05-07 14:14:32 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:14:36 Jiminny\Jobs\Crm\SyncObjects [PASSWORD_DOTS] 3s DONE
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:15:03 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:06 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:08 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:11 Running ['artisan' mailbox:skip-lists:refresh] . 6s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:18 Running ['artisan' mailbox:batch:process --max-batches=15] 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:22 Running ['artisan' activity:purge-stale] ....... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:24 Running ['artisan' mailbox:text-relay:sync] {
docker_lamp_1 | "error": "invalid_request",
docker_lamp_1 | "error_description": "Invalid impersonation \u0026quot;sub\u0026quot; field: @"
docker_lamp_1 | }
docker_lamp_1 | .... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:28 Running ['artisan' conference:pre-meeting-notification] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:30 Running ['artisan' conference:monitor:start] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:33 Running ['artisan' conference:monitor:end] ..... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:end > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:34 Running ['artisan' jiminny:fix-hubspot-tokens] . 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:fix-hubspot-tokens > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:38 Running ['artisan' conference:pre-meeting-reminder] in background 19.64ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' conference:pre-meeting-reminder > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-805efb160ee8d9da02e60364ace7970eb2b35f31" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:15:38 Running ['artisan' hubspot:journal-poll --start] in background 2.93ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' hubspot:journal-poll --start > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-e26d77f915d2c55fe91ca4148a230e32eaa1865e" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:15:38 Running ['artisan' datadog:report:processing-sla-activities] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' datadog:report:processing-sla-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:41 Running ['artisan' activity:sync --from='2026-05-07 13:59:00' --to='2026-05-07 14:15:00' --skipProviders='ringcentral' --skipProviders='avaya' --skipProviders='telus' --skipProviders='talkdesk'] 🚀 Starting HubSpot journal polling service...
docker_lamp_1 | 6s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:sync --from='2026-05-07 13:59:00' --to='2026-05-07 14:15:00' --skipProviders='ringcentral' --skipProviders='avaya' --skipProviders='telus' --skipProviders='talkdesk' > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:47 Running ['artisan' crm:bullhorn:ping --heartbeat] 2026-05-07 14:15:49 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:15:49 Jiminny\Jobs\Activity\SyncActivity ....... 105.02ms DONE
docker_lamp_1 | 2026-05-07 14:15:49 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 0 social account(s) to be processed ...
docker_lamp_1 |
docker_lamp_1 | Done!
docker_lamp_1 | 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:49 Running ['artisan' nudges:send --silent] 2026-05-07 14:15:49 Jiminny\Notifications\Crm\UserAccountDisconnected RUNNING
docker_lamp_1 | 2026-05-07 14:15:49 Jiminny\Notifications\Crm\UserAccountDisconnected 225.37ms DONE
docker_lamp_1 | 2026-05-07 14:15:50 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-05-07 14:15:50 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:15:51 Jiminny\Jobs\Activity\SyncActivity ....... 632.90ms DONE
docker_lamp_1 | 2026-05-07 14:15:51 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:15:52 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 2026-05-07 14:15:52 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:15:52 Jiminny\Jobs\Activity\SyncActivity ....... 485.99ms DONE
docker_lamp_1 | 2026-05-07 14:15:52 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] RUNNING
docker_lamp_1 | ....... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' nudges:send --silent > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:15:52 Running ['artisan' jiminny:playlists:normalize-sort] 2026-05-07 14:15:54 Jiminny\Jobs\Activity\SyncActivity [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:playlists:normalize-sort > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:16:03 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:05 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:08 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:10 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:12 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:15 Running ['artisan' conference:monitor:count] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:17 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-05-07 12:51:19 (delay: 0s)
docker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)
docker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)
docker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)
docker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...
docker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...
docker_lamp_1 | Dispatched 4 HubSpot sync jobs
docker_lamp_1 | ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:20 Running ['artisan' activity:notify-not-logged] 2026-05-07 14:16:20 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] 1s FAIL
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 131.73ms DONE
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 84.82ms DONE
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 123.16ms DONE
docker_lamp_1 | 2026-05-07 14:16:22 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:23 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 407.87ms FAIL
docker_lamp_1 | 2026-05-07 14:16:23 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:24 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 600.83ms FAIL
docker_lamp_1 | . 5s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:notify-not-logged > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:25 Running ['artisan' activity:status-count] {"canceled":28}...... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:status-count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:16:28 Running ['artisan' mailbox:sync] Queueing 2 inbox(es) for sync.
docker_lamp_1 | [PASSWORD_DOTS] 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-05-07 14:16:36 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:36 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] 130.04ms DONE
docker_lamp_1 | 2026-05-07 14:16:36 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:16:36 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] 88.95ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:17:11 Running ['artisan' meeting-bot:schedule-bot] ... 8s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:17:20 Running ['artisan' dialers:monitor-activities] . 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:17:24 Running ['artisan' jiminny:monitor-social-accounts] 5s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:17:30 Running ['artisan' mailbox:skip-lists:refresh] . 6s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:17:36 Running ['artisan' mailbox:batch:process --max-batches=15] 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:17:40 Running ['artisan' mailbox:batch:create] ....... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-05-07 14:17:43 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:17:43 Jiminny\Jobs\Mailbox\CreateBatches ....... 137.19ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:18:05 Running ['artisan' meeting-bot:schedule-bot] ... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:18:08 Running ['artisan' dialers:monitor-activities] . 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:18:13 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:18:15 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:18:18 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:18:20 Running ['artisan' conference:monitor:count] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:18:23 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 22.14ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:18:23 Running ['artisan' crm:autolog-delayed] Dispatched autolog delayed jobs for all applicable teams:
docker_lamp_1 | [PASSWORD_DOTS] 7s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:autolog-delayed > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:19:03 Running ['artisan' meeting-bot:schedule-bot] ... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:19:07 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:19:09 Running ['artisan' jiminny:monitor-social-accounts] 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:19:13 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:19:15 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:20:05 Running ['artisan' meeting-bot:schedule-bot] ... 5s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:10 Running ['artisan' dialers:monitor-activities] . 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:15 Running ['artisan' jiminny:monitor-social-accounts] 6s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:21 Running ['artisan' mailbox:skip-lists:refresh] . 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:25 Running ['artisan' mailbox:batch:process --max-batches=15] 2026-05-07 14:20:29,636 WARN stopped: jiminny-worker-processing-3_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,637 WARN stopped: jiminny-worker-processing-2_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,638 WARN stopped: jiminny-worker-processing-4_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,638 WARN stopped: jiminny-worker-processing-5_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,638 WARN stopped: jiminny-worker-processing-delayed_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,639 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,639 INFO waiting for jiminny-worker-processing-1_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-analytics_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-audio_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-calendar_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-conferences_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-crm-sync_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-crm-update_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-download_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-emails_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-es-update_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 INFO waiting for worker-nudges_00 to stop
docker_lamp_1 | 2026-05-07 14:20:29,640 WARN stopped: worker-crm-update_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,641 WARN stopped: worker-analytics_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,641 WARN stopped: worker-download_00 (terminated by SIGTERM)
docker_lamp_1 | 2026-05-07 14:20:29,641 WARN stopped: worker-nudges_00 (terminated by SIGTERM)
docker_lamp_1 | run_artisan_schedule: Caught SIGTERM signal!
docker_lamp_1 | 2026-05-07 14:20:29,988 INFO stopped: worker-conferences_00 (exit status 0)
docker_lamp_1 | 2026-05-07 14:20:29,988 INFO stopped: jiminny-worker-processing-1_00 (exit status 0)
docker_lamp_1 | 2026-05-07 14:20:29,988 INFO stopped: worker_00 (exit status 0)
docker_lamp_1 | 2026-05-07 14:20:29,994 INFO stopped: worker-audio_00 (exit status 0)
docker_lamp_1 | 2026-05-07 14:20:30,001 INFO stopped: worker-crm-sync_00 (exit status 0)
docker_lamp_1 | 2026-05-07 14:20:30,015 INFO stopped: worker-emails_00 (exit status 0)
docker_lamp_1 | 2026-05-07 14:20:30,045 INFO stopped: worker-calendar_00 (exit status 0)
docker_lamp_1 |
docker_lamp_1 | Hostname docker_lamp_1 pid 49888 gracefully stopped.
docker_lamp_1 | 2026-05-07 14:20:30,906 INFO stopped: worker-es-update_00 (exit status 0)
docker_lamp_1 | 6s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:31 Running ['artisan' conference:monitor:count] 2026-05-07 14:20:32,430 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | 2026-05-07 14:20:34,440 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | ... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:35 Running ['artisan' activity:purge-stale] 2026-05-07 14:20:37,433 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | ....... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:38 Running ['artisan' mailbox:text-relay:sync] 2026-05-07 14:20:39,479 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | {
docker_lamp_1 | "error": "invalid_request",
docker_lamp_1 | "error_description": "Invalid impersonation \u0026quot;sub\u0026quot; field: @"
docker_lamp_1 | }
docker_lamp_1 | 2026-05-07 14:20:41,492 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | .... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:41 Running ['artisan' conference:pre-meeting-notification] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:43 Running ['artisan' conference:monitor:start] 2026-05-07 14:20:44,236 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:45 Running ['artisan' conference:monitor:end] 2026-05-07 14:20:46,400 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | 2026-05-07 14:20:48,419 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | ..... 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:end > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:49 Running ['artisan' jiminny:fix-hubspot-tokens] 2026-05-07 14:20:50,634 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | 2026-05-07 14:20:52,639 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | . 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:fix-hubspot-tokens > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:53 Running ['artisan' conference:pre-meeting-reminder] in background 3.09ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' conference:pre-meeting-reminder > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-805efb160ee8d9da02e60364ace7970eb2b35f31" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:20:53 Running ['artisan' hubspot:journal-poll --start] in background 1.81ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' hubspot:journal-poll --start > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-e26d77f915d2c55fe91ca4148a230e32eaa1865e" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:20:53 Running ['artisan' jiminny:transcription:retry-failed] 2026-05-07 14:20:55,261 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | No failed transcriptions found.
docker_lamp_1 | 2026-05-07 14:20:57,268 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:transcription:retry-failed > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:57 Running ['artisan' crm:reset-governor] 🚀 Starting HubSpot journal polling service...
docker_lamp_1 | 2026-05-07 14:20:59,311 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | [PASSWORD_DOTS] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:reset-governor > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:20:59 Running ['artisan' crm:bullhorn:ping --heartbeat] 2026-05-07 14:21:01,909 INFO waiting for artisan-schedule_00 to stop
docker_lamp_1 | 0 social account(s) to be processed ...
docker_lamp_1 |
docker_lamp_1 | Done!
docker_lamp_1 | 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run after SIGTERM
docker_lamp_1 | 2026-05-07 14:21:02,331 WARN stopped: artisan-schedule_00 (exit status 143)
docker_lamp_1 | 2026-05-07 14:21:02,373 INFO spawned: 'artisan-schedule_00' with pid 53379
docker_lamp_1 | 2026-05-07 14:21:02,390 INFO spawned: 'jiminny-worker-processing-1_00' with pid 53380
docker_lamp_1 | 2026-05-07 14:21:02,401 INFO spawned: 'jiminny-worker-processing-2_00' with pid 53381
docker_lamp_1 | 2026-05-07 14:21:02,406 INFO spawned: 'jiminny-worker-processing-3_00' with pid 53382
docker_lamp_1 | 2026-05-07 14:21:02,413 INFO spawned: 'jiminny-worker-processing-4_00' with pid 53386
docker_lamp_1 | 2026-05-07 14:21:02,422 INFO spawned: 'jiminny-worker-processing-5_00' with pid 53388
docker_lamp_1 | 2026-05-07 14:21:02,425 INFO spawned: 'jiminny-worker-processing-delayed_00' with pid 53390
docker_lamp_1 | 2026-05-07 14:21:02,437 INFO spawned: 'worker_00' with pid 53392
docker_lamp_1 | 2026-05-07 14:21:02,440 INFO spawned: 'worker-analytics_00' with pid 53394
docker_lamp_1 | 2026-05-07 14:21:02,445 INFO spawned: 'worker-audio_00' with pid 53396
docker_lamp_1 | 2026-05-07 14:21:02,448 INFO spawned: 'worker-calendar_00' with pid 53397
docker_lamp_1 | 2026-05-07 14:21:02,452 INFO spawned: 'worker-conferences_00' with pid 53398
docker_lamp_1 | 2026-05-07 14:21:02,454 INFO spawned: 'worker-crm-sync_00' with pid 53399
docker_lamp_1 | 2026-05-07 14:21:02,456 INFO spawned: 'worker-crm-update_00' with pid 53400
docker_lamp_1 | 2026-05-07 14:21:02,463 INFO spawned: 'worker-download_00' with pid 53401
docker_lamp_1 | 2026-05-07 14:21:02,474 INFO spawned: 'worker-emails_00' with pid 53404
docker_lamp_1 | 2026-05-07 14:21:02,507 INFO spawned: 'worker-es-update_00' with pid 53405
docker_lamp_1 | 2026-05-07 14:21:02,514 INFO spawned: 'worker-nudges_00' with pid 53406
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: artisan-schedule_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-1_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-2_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-3_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-4_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-5_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: jiminny-worker-processing-delayed_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-analytics_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-audio_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-calendar_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-conferences_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-crm-sync_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-crm-update_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-download_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-emails_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-es-update_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 | 2026-05-07 14:21:03,517 INFO success: worker-nudges_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:22:03 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:05 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:07 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:11 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:12 Running ['artisan' conference:monitor:count] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:14 Running ['artisan' mailbox:batch:create] ....... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:16 Running ['artisan' activity:sync 'ringcentral' 'avaya' 'telus' 'talkdesk' --from='2026-05-07 14:06:00' --to='2026-05-07 14:22:00'] 2026-05-07 14:22:18 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:22:19 Jiminny\Jobs\Mailbox\CreateBatches ....... 745.06ms DONE
docker_lamp_1 | 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:sync 'ringcentral' 'avaya' 'telus' 'talkdesk' --from='2026-05-07 14:06:00' --to='2026-05-07 14:22:00' > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:20 Running ['artisan' twilio:recover-tracks] ...... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' twilio:recover-tracks > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:21 Running ['artisan' dialers:sync-users] [PASSWORD_DOTS] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:sync-users > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:22:23 Running ['artisan' datadog:report:failed-processing-states] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' datadog:report:failed-processing-states > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:23:03 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:23:05 Running ['artisan' dialers:monitor-activities] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:23:07 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:23:09 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:23:10 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:23:13 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 8.67ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:23:13 Running ['artisan' crm:integration-app-validate-team-connection] Parameter `teamId` is not provided. Loading all teams using IntegrationApp.
docker_lamp_1 | 1 team connections to be validated.
docker_lamp_1 | 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:integration-app-validate-team-connection > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-05-07 14:23:17 Jiminny\Services\Crm\IntegrationApp\Jobs\ValidateTeamActiveConnectionJob RUNNING
docker_lamp_1 | 2026-05-07 14:23:18 Jiminny\Services\Crm\IntegrationApp\Jobs\ValidateTeamActiveConnectionJob 751.64ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:24:05 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:08 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:11 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:13 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:16 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:18 Running ['artisan' conference:monitor:count] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:20 Running ['artisan' activity:aircall:check-and-renew] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:aircall:check-and-renew > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:24:22 Running ['artisan' track:retry-failed-downloads] 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' track:retry-failed-downloads > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:25:04 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:06 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:09 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:12 Running ['artisan' mailbox:skip-lists:refresh] . 7s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:20 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:22 Running ['artisan' activity:purge-stale] ....... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:purge-stale > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:26 Running ['artisan' mailbox:text-relay:sync] {
docker_lamp_1 | "error": "invalid_request",
docker_lamp_1 | "error_description": "Invalid impersonation \u0026quot;sub\u0026quot; field: @"
docker_lamp_1 | }
docker_lamp_1 | .... 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:text-relay:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:29 Running ['artisan' conference:pre-meeting-notification] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:pre-meeting-notification > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:32 Running ['artisan' conference:monitor:start] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:start > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:33 Running ['artisan' conference:monitor:end] ..... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:end > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:36 Running ['artisan' jiminny:fix-hubspot-tokens] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:fix-hubspot-tokens > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:25:38 Running ['artisan' conference:pre-meeting-reminder] in background 2.53ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' conference:pre-meeting-reminder > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-805efb160ee8d9da02e60364ace7970eb2b35f31" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:25:38 Running ['artisan' hubspot:journal-poll --start] in background 1.58ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' hubspot:journal-poll --start > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-e26d77f915d2c55fe91ca4148a230e32eaa1865e" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:25:38 Running ['artisan' crm:bullhorn:ping --heartbeat] 0 social account(s) to be processed ...
docker_lamp_1 |
docker_lamp_1 | Done!
docker_lamp_1 | 🚀 Starting HubSpot journal polling service...
docker_lamp_1 | 4s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:bullhorn:ping --heartbeat > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:26:03 Running ['artisan' meeting-bot:schedule-bot] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:05 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:08 Running ['artisan' jiminny:monitor-social-accounts] 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:11 Running ['artisan' mailbox:skip-lists:refresh] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:14 Running ['artisan' mailbox:batch:process --max-batches=15] 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:17 Running ['artisan' conference:monitor:count] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:19 Running ['artisan' crm:sync-hubspot-objects] [HubSpot] Syncing objects for Hubspot (abae74b8-bfa8-4383-9a7f-89f4bf2bdbb4) since 2026-05-07 12:51:19 (delay: 0s)
docker_lamp_1 | Team TestV (2708d27a-7f31-4d90-bd2b-b5bde1c0211e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team KioskAccount (dedab245-604c-4cd2-a83e-4b9e034b2772) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for InsightSquared (b2b115eb-93ce-4d1b-929c-173757df8fba) since 2026-02-17 15:09:59 (delay: 0s)
docker_lamp_1 | Team Aircall Demo (7980e5eb-b11c-4cee-9c21-8bb29ba85f3b) is not yet assigned an owner. skipping...
docker_lamp_1 | [HubSpot] Syncing objects for GoStudent UAT (b2d49a54-b645-4637-a7ae-a86cfce6e8e4) since 2026-02-17 15:09:59 (delay: 1s)
docker_lamp_1 | [HubSpot] Syncing objects for JustCall (c6b9d6b0-b48d-4832-a68c-a57d60651888) since 2026-02-17 15:07:41 (delay: 1s)
docker_lamp_1 | Team Twilio Video (c334ca55-b230-411c-b10e-31c8204bd07b) is not yet assigned an owner. skipping...
docker_lamp_1 | Team My Test Account 3000 (dbc9990d-b35f-4e38-9550-22cdd6059514) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test (7997eb70-8aa4-491a-870d-311977568df4) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Test (5e06dcee-0613-470e-9a77-2c283198f3bf) is not yet assigned an owner. skipping...
docker_lamp_1 | Team test ogg auto sync (da44776e-306f-427a-83d8-a1b4baa5537e) is not yet assigned an owner. skipping...
docker_lamp_1 | Team Tourlaner (d9b71080-388b-4cf5-8175-aa0f29bee635) is not yet assigned an owner. skipping...
docker_lamp_1 | Dispatched 4 HubSpot sync jobs
docker_lamp_1 | ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' crm:sync-hubspot-objects > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:22 Running ['artisan' activity:notify-not-logged] 2026-05-07 14:26:23 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:23 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 417.37ms FAIL
docker_lamp_1 | 2026-05-07 14:26:23 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:23 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 55.56ms DONE
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 39.36ms DONE
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects ....... 23.82ms DONE
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 265.85ms FAIL
docker_lamp_1 | 2026-05-07 14:26:24 Jiminny\Jobs\Crm\SyncHubspotObjects [PASSWORD_DOTS] RUNNING
docker_lamp_1 | . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:notify-not-logged > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:24 Running ['artisan' activity:status-count] 2026-05-07 14:26:25 Jiminny\Jobs\Crm\SyncHubspotObjects ...... 620.43ms FAIL
docker_lamp_1 | {"canceled":28}...... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' activity:status-count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:26:27 Running ['artisan' mailbox:sync] Queueing 2 inbox(es) for sync.
docker_lamp_1 | [PASSWORD_DOTS] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:sync > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-05-07 14:26:30 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:30 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] 55.28ms DONE
docker_lamp_1 | 2026-05-07 14:26:30 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:26:30 Jiminny\Jobs\Mailbox\SyncInbox [PASSWORD_DOTS] 51.97ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:27:03 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:27:04 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:27:07 Running ['artisan' jiminny:monitor-social-accounts] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:27:10 Running ['artisan' mailbox:skip-lists:refresh] . 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:27:12 Running ['artisan' mailbox:batch:process --max-batches=15] 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:27:14 Running ['artisan' mailbox:batch:create] ....... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:create > '/proc/1/fd/1' 2>&1
docker_lamp_1 |
docker_lamp_1 | run_artisan_schedule: Done waiting for schedule:run
docker_lamp_1 | 2026-05-07 14:27:18 Jiminny\Jobs\Mailbox\CreateBatches [PASSWORD_DOTS] RUNNING
docker_lamp_1 | 2026-05-07 14:27:18 Jiminny\Jobs\Mailbox\CreateBatches ....... 203.80ms DONE
docker_lamp_1 |
docker_lamp_1 | 2026-05-07 14:28:04 Running ['artisan' meeting-bot:schedule-bot] ... 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' meeting-bot:schedule-bot > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:28:06 Running ['artisan' dialers:monitor-activities] . 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' dialers:monitor-activities > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:28:09 Running ['artisan' jiminny:monitor-social-accounts] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' jiminny:monitor-social-accounts > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:28:11 Running ['artisan' mailbox:skip-lists:refresh] . 3s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:skip-lists:refresh > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:28:14 Running ['artisan' mailbox:batch:process --max-batches=15] 1s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' mailbox:batch:process --max-batches=15 > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:28:15 Running ['artisan' conference:monitor:count] ... 2s DONE
docker_lamp_1 | ⇂ '/usr/local/bin/php' 'artisan' conference:monitor:count > '/proc/1/fd/1' 2>&1
docker_lamp_1 | 2026-05-07 14:28:18 Running ['artisan' mailbox:batch:retry-failed --max-batches=15] in background 3.59ms DONE
docker_lamp_1 | ⇂ ('/usr/local/bin/php' 'artisan' mailbox:batch:retry-failed --max-batches=15 > '/proc/1/fd/1' 2>&1 ; '/usr/local/bin/php' 'artisan' schedule:finish "framework/schedule-390defd641effba0f73a895e426ded4cf2ba7f11" "$?") > '/dev/null' 2>&1 &
docker_lamp_1 | 2026-05-07 14:28:18 Running ['artisan' calendar:sync --dateMode=daily] 2026-05-07 14:28:28 Jiminny\Jobs\Calendar\SyncCalendarE...
|
4825
|
NULL
|
NULL
|
NULL
|
|
4830
|
173
|
38
|
2026-05-07T14:31:01.972698+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164261972_m1.jpg...
|
PhpStorm
|
faVsco.js – PayloadBuilder.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] string $phone The phone number to search.
* @param bool $isAlternativeSearch Indicates if an alternative search should be performed
* if the first search fails to find a match. The first search request should cover most of the cases.
*/
public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array
{
$filterPropertyNames = $isAlternativeSearch ?
['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :
['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];
$filterGroups = array_map(function ($propertyName) use ($phone) {
return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);
}, $filterPropertyNames);
$properties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
return $this->createPayload($filterGroups, $properties);
}
/**
* Creates a filter group with the specified parameters.
*/
private function createFilterGroup(string $propertyName, string $operator, $value): array
{
return [
'filters' => [
[
'propertyName' => $propertyName,
'operator' => $operator,
'value' => $value,
],
],
];
}
/**
* Creates a payload with the specified filter groups and properties.
*/
private function createPayload(array $filterGroups, array $properties): array
{
return [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => 'modifieddate',
'direction' => 'DESCENDING',
],
],
'properties' => $properties,
'limit' => 1,
];
}
/**
* Generates a payload to find related activities based on the specified data and object type.
* The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)
* and are associated with any of the provided prospect types (contact or company).
*
* @param array $data An associative array containing the following keys:
* - 'from': The start time for the activity search (timestamp).
* - 'to': The end time for the activity search (timestamp).
* - 'contact': (optional) The ID of the associated contact.
* - 'company': (optional) The ID of the associated company.
* @param string $objectType The type of engagement to search for (meeting or other type like call).
*
* @return array The payload array to be used for searching related activities.
*/
public function getFindRelatedActivityPayload(array $data, string $objectType): array
{
$payload = [
'sorts' => [
[
'propertyName' => self::SORT_PROPERTY,
'direction' => 'DESCENDING',
],
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
if ($objectType === self::ENGAGEMENT_MEETINGS) {
$payload['properties'] = [
'hs_meeting_title',
'hs_meeting_outcome',
'hs_activity_type',
'hs_timestamp',
'hubspot_owner_id',
'hs_meeting_body',
'hs_internal_meeting_notes',
'hs_meeting_location',
'hs_meeting_start_time',
'hs_meeting_end_time',
];
} else {
$payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];
}
$timeFiltersWithEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'LTE',
'value' => $data['to'],
],
];
$timeFiltersWithoutEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'NOT_HAS_PROPERTY',
],
];
$payload['filterGroups'] = [];
$associationTypes = ['contact', 'company'];
foreach ($associationTypes as $type) {
if (! empty($data[$type])) {
$filterGroupWithEndTime = [
'filters' => array_merge(
$timeFiltersWithEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithEndTime;
$filterGroupWithoutEndTime = [
'filters' => array_merge(
$timeFiltersWithoutEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithoutEndTime ;
}
}
return $payload;
}
/** Parameters
* [
* 'accountId' => $crmAccountId,
* 'sortBy' => $sortBy,
* 'sortDir' => $sortDir,
* 'onlyOpen' => $onlyOpen,
* 'closedStages' => $this->getClosedDealStages(),
* 'userId' => $userId,
* ];
*/
public function generateOpportunitiesSearchPayload(
Configuration $config,
string $crmAccountId,
array $closedStages,
): array {
$closedFilters = [];
$filterGroups = [];
$onlyOpen = true;
switch ($config->getOpportunityAssignmentRule()) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$sortBy = 'createdate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$sortBy = 'createdate';
$sortDir = 'ASCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
default:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
$onlyOpen = false;
}
$baseFilters = [
[
'propertyName' => 'associations.company',
'operator' => 'EQ',
'value' => $crmAccountId,
],
];
// Handle closed stages in chunks
if ($onlyOpen) {
foreach (['won', 'lost'] as $key) {
if (! empty($closedStages[$key])) {
$chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);
foreach ($chunks as $chunk) {
$closedFilters[] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $chunk,
];
}
}
}
}
$filterGroups[] = [
'filters' => array_merge($baseFilters, $closedFilters),
];
$payload = [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => $sortBy,
'direction' => $sortDir,
],
],
'properties' => [
'dealname',
'amount',
'hubspot_owner_id',
'pipeline',
'dealstage',
'closedate',
'deal_currency_code',
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
$logger = app(LoggerInterface::class);
$logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
return $payload;
}
/**
* Converts v1 payload data to v3
*/
public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array
{
// Use this mapping until the whole Hubspot V1 is dropped.
$properties = [
'hubspot_owner_id' => $engagement['ownerId'],
'hs_timestamp' => $engagement['timestamp'],
];
// $engagement['activityType'] is $activity->category->name
if (isset($engagement['activityType'])) {
$properties['hs_activity_type'] = $engagement['activityType'];
}
$metadataKeyMap = [
'hs_meeting_outcome' => 'meetingOutcome',
'hs_meeting_title' => 'title',
'hs_meeting_start_time' => 'startTime',
'hs_meeting_end_time' => 'endTime',
'hs_meeting_body' => 'body',
'hs_internal_meeting_notes' => 'internalMeetingNotes',
];
foreach ($metadataKeyMap as $newKey => $oldKey) {
if (isset($metadata[$oldKey])) {
$properties[$newKey] = $metadata[$oldKey];
unset($metadata[$oldKey]);
}
}
$properties = [
...$properties,
...$metadata, // custom fields
];
$response = [
'properties' => $properties,
];
if (! empty($associations)) {
$response['associations'] = $associations;
}
return $response;
}
/**
* Generate a payload to search for contacts by name.
* The search is a token search in firstname or lastname
*/
public function generateSearchContactsByNamePayload(string $name, array $fields): array
{
$firstNameFilter = [
'propertyName' => 'firstname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
$lastNameFilter = [
'propertyName' => 'lastname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
return [
'filterGroups' => [
[
'filters' => [$firstNameFilter],
],
[
'filters' => [$lastNameFilter],
],
],
'properties' => $fields,
'sorts' => [
[
'propertyName' => 'lastmodifieddate',
'direction' => 'DESCENDING',
],
],
];
}
public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array
{
$inputs = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$inputs[] = [
'from' => [
'id' => $crmId,
],
'to' => [
'id' => (string) $id,
],
'types' => [
[
'associationCategory' => 'HUBSPOT_DEFINED',
'associationTypeId' => $associationType,
],
],
];
}
return ['inputs' => $inputs];
}
public function buildRemoveAssociationPayload(string $crmId, array $ids): array
{
$toArray = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$toArray[] = ['id' => (string) $id];
}
return [
'inputs' => [
[
'from' => ['id' => $crmId],
'to' => $toArray,
],
],
];
}
public function addClosedStageFilters(array &$payload, array $closedStages): void
{
if (! empty($closedStages['won'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['won'],
];
}
if (! empty($closedStages['lost'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['lost'],
];
}
}
public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void
{
$payload['filters'][] = [
'propertyName' => 'createdate',
'operator' => 'GT',
'value' => $createdAfter->getPreciseTimestamp(3),
];
}
public function getDealsInBulkPayload(array $dealIds): array
{
return [
'filterGroups' => [
[
'filters' => [
[
'propertyName' => 'hs_object_id',
'operator' => 'IN',
'values' => $dealIds,
],
],
],
],
'limit' => self::MAX_FILTER_SIZE,
];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"25","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Psr\\Log\\LoggerInterface;\n\nclass PayloadBuilder\n{\n public const int MAX_SEARCH_REQUEST_LIMIT = 200;\n\n private const int MAX_FILTER_SIZE = 100;\n\n private const string SORT_PROPERTY = 'hs_timestamp';\n private const string ENGAGEMENT_MEETINGS = 'meetings';\n\n public function getLinkToTaskPayload(string $objectType, string $objectId, string $engagementType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($engagementType === self::ENGAGEMENT_MEETINGS) {\n $payload['filterGroups'] = $this->buildMeetingFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_timestamp',\n 'hs_activity_type',\n ];\n } else {\n $payload['filterGroups'] = $this->buildTaskFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp'];\n }\n\n return $payload;\n }\n\n public function getRecentlyUpdatedSearchPayload(Carbon $since, ?Carbon $to, array $properties): array\n {\n $lastUpdateDate = in_array('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n public function getByProfileSearchPayload(string $crmId, array $properties, Carbon $since, ?Carbon $to): array\n {\n $lastUpdateDate = array_key_exists('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n [\n 'propertyName' => 'hubspot_owner_id',\n 'operator' => 'EQ',\n 'value' => $crmId,\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n private function buildMeetingFiltersForLinkToTask($objectType, $objectId): array\n {\n $filters = [];\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'IN',\n 'values' => ['SCHEDULED', 'RESCHEDULED'],\n ],\n ],\n ];\n\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ],\n ];\n\n return $filters;\n }\n\n private function buildTaskFiltersForLinkToTask($objectType, $objectId): array\n {\n return [\n [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n ],\n ],\n ];\n }\n\n private function getAssociatedObjectFilter(string $objectType, string $objectId): array\n {\n return [\n 'propertyName' => 'associations.' . $objectType,\n 'operator' => 'EQ',\n 'value' => $objectId,\n ];\n }\n\n public function generatePlaybackURLSearchPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n public function generateGetCallsPayload(Carbon $from, Carbon $to, string $activityProvider, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $activityProvider,\n ],\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function generateSearchCallsByPeriodPayload(Carbon $from, Carbon $to, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $this->getSearchCallAttributes(),\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function getSearchCallAttributes(): array\n {\n return [\n 'hs_timestamp',\n 'hs_call_recording_url',\n 'hs_call_body',\n 'hs_call_status',\n 'hs_call_to_number',\n 'hs_call_from_number',\n 'hs_call_duration',\n 'hs_call_disposition',\n 'hs_call_title',\n 'hs_call_direction',\n 'hubspot_owner_id',\n 'hs_activity_type',\n 'hs_call_external_id',\n 'hs_call_source',\n ];\n }\n\n public function generatePlaybackAddUrlBatchPayload(array $crmUpdateData): array\n {\n $updateObjectsData = [];\n\n foreach ($crmUpdateData as $data) {\n $updateObjectsData[] = [\n 'id' => $data['crm_id'],\n 'properties' => [\n 'hs_call_body' => $data['hs_call_body'] .\n '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">' .\n '<a href=\"' . $data['playback_url'] . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>',\n ],\n ];\n }\n\n return ['inputs' => $updateObjectsData];\n }\n\n public function generateSearchCallByTokenPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload for phone search based on the specified parameters.\n *\n * @param string $phone The phone number to search.\n * @param bool $isAlternativeSearch Indicates if an alternative search should be performed\n * if the first search fails to find a match. The first search request should cover most of the cases.\n */\n public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array\n {\n $filterPropertyNames = $isAlternativeSearch ?\n ['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :\n ['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];\n\n $filterGroups = array_map(function ($propertyName) use ($phone) {\n return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);\n }, $filterPropertyNames);\n\n $properties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n\n return $this->createPayload($filterGroups, $properties);\n }\n\n /**\n * Creates a filter group with the specified parameters.\n */\n private function createFilterGroup(string $propertyName, string $operator, $value): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => $propertyName,\n 'operator' => $operator,\n 'value' => $value,\n ],\n ],\n ];\n }\n\n /**\n * Creates a payload with the specified filter groups and properties.\n */\n private function createPayload(array $filterGroups, array $properties): array\n {\n return [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $properties,\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload to find related activities based on the specified data and object type.\n * The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)\n * and are associated with any of the provided prospect types (contact or company).\n *\n * @param array $data An associative array containing the following keys:\n * - 'from': The start time for the activity search (timestamp).\n * - 'to': The end time for the activity search (timestamp).\n * - 'contact': (optional) The ID of the associated contact.\n * - 'company': (optional) The ID of the associated company.\n * @param string $objectType The type of engagement to search for (meeting or other type like call).\n *\n * @return array The payload array to be used for searching related activities.\n */\n public function getFindRelatedActivityPayload(array $data, string $objectType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($objectType === self::ENGAGEMENT_MEETINGS) {\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_activity_type',\n 'hs_timestamp',\n 'hubspot_owner_id',\n 'hs_meeting_body',\n 'hs_internal_meeting_notes',\n 'hs_meeting_location',\n 'hs_meeting_start_time',\n 'hs_meeting_end_time',\n ];\n } else {\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];\n }\n\n $timeFiltersWithEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'LTE',\n 'value' => $data['to'],\n ],\n ];\n\n $timeFiltersWithoutEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ];\n\n $payload['filterGroups'] = [];\n\n $associationTypes = ['contact', 'company'];\n foreach ($associationTypes as $type) {\n if (! empty($data[$type])) {\n $filterGroupWithEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithEndTime;\n\n $filterGroupWithoutEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithoutEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithoutEndTime ;\n }\n }\n\n return $payload;\n }\n\n /** Parameters\n * [\n * 'accountId' => $crmAccountId,\n * 'sortBy' => $sortBy,\n * 'sortDir' => $sortDir,\n * 'onlyOpen' => $onlyOpen,\n * 'closedStages' => $this->getClosedDealStages(),\n * 'userId' => $userId,\n * ];\n */\n public function generateOpportunitiesSearchPayload(\n Configuration $config,\n string $crmAccountId,\n array $closedStages,\n ): array {\n $closedFilters = [];\n $filterGroups = [];\n $onlyOpen = true;\n\n switch ($config->getOpportunityAssignmentRule()) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'ASCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n default:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n $onlyOpen = false;\n }\n\n $baseFilters = [\n [\n 'propertyName' => 'associations.company',\n 'operator' => 'EQ',\n 'value' => $crmAccountId,\n ],\n ];\n\n // Handle closed stages in chunks\n if ($onlyOpen) {\n foreach (['won', 'lost'] as $key) {\n if (! empty($closedStages[$key])) {\n $chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);\n foreach ($chunks as $chunk) {\n $closedFilters[] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $chunk,\n ];\n }\n }\n }\n }\n\n $filterGroups[] = [\n 'filters' => array_merge($baseFilters, $closedFilters),\n ];\n\n $payload = [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => $sortBy,\n 'direction' => $sortDir,\n ],\n ],\n 'properties' => [\n 'dealname',\n 'amount',\n 'hubspot_owner_id',\n 'pipeline',\n 'dealstage',\n 'closedate',\n 'deal_currency_code',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n $logger = app(LoggerInterface::class);\n $logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * Converts v1 payload data to v3\n */\n public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array\n {\n // Use this mapping until the whole Hubspot V1 is dropped.\n $properties = [\n 'hubspot_owner_id' => $engagement['ownerId'],\n 'hs_timestamp' => $engagement['timestamp'],\n ];\n\n // $engagement['activityType'] is $activity->category->name\n if (isset($engagement['activityType'])) {\n $properties['hs_activity_type'] = $engagement['activityType'];\n }\n\n $metadataKeyMap = [\n 'hs_meeting_outcome' => 'meetingOutcome',\n 'hs_meeting_title' => 'title',\n 'hs_meeting_start_time' => 'startTime',\n 'hs_meeting_end_time' => 'endTime',\n 'hs_meeting_body' => 'body',\n 'hs_internal_meeting_notes' => 'internalMeetingNotes',\n ];\n\n foreach ($metadataKeyMap as $newKey => $oldKey) {\n if (isset($metadata[$oldKey])) {\n $properties[$newKey] = $metadata[$oldKey];\n unset($metadata[$oldKey]);\n }\n }\n\n $properties = [\n ...$properties,\n ...$metadata, // custom fields\n ];\n\n $response = [\n 'properties' => $properties,\n ];\n\n if (! empty($associations)) {\n $response['associations'] = $associations;\n }\n\n return $response;\n }\n\n /**\n * Generate a payload to search for contacts by name.\n * The search is a token search in firstname or lastname\n */\n public function generateSearchContactsByNamePayload(string $name, array $fields): array\n {\n $firstNameFilter = [\n 'propertyName' => 'firstname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n $lastNameFilter = [\n 'propertyName' => 'lastname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n return [\n 'filterGroups' => [\n [\n 'filters' => [$firstNameFilter],\n ],\n [\n 'filters' => [$lastNameFilter],\n ],\n ],\n 'properties' => $fields,\n 'sorts' => [\n [\n 'propertyName' => 'lastmodifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n ];\n }\n\n public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array\n {\n $inputs = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $inputs[] = [\n 'from' => [\n 'id' => $crmId,\n ],\n 'to' => [\n 'id' => (string) $id,\n ],\n 'types' => [\n [\n 'associationCategory' => 'HUBSPOT_DEFINED',\n 'associationTypeId' => $associationType,\n ],\n ],\n ];\n }\n\n return ['inputs' => $inputs];\n }\n\n public function buildRemoveAssociationPayload(string $crmId, array $ids): array\n {\n $toArray = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $toArray[] = ['id' => (string) $id];\n }\n\n return [\n 'inputs' => [\n [\n 'from' => ['id' => $crmId],\n 'to' => $toArray,\n ],\n ],\n ];\n }\n\n public function addClosedStageFilters(array &$payload, array $closedStages): void\n {\n if (! empty($closedStages['won'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['won'],\n ];\n }\n\n if (! empty($closedStages['lost'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['lost'],\n ];\n }\n }\n\n public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void\n {\n $payload['filters'][] = [\n 'propertyName' => 'createdate',\n 'operator' => 'GT',\n 'value' => $createdAfter->getPreciseTimestamp(3),\n ];\n }\n\n public function getDealsInBulkPayload(array $dealIds): array\n {\n return [\n 'filterGroups' => [\n [\n 'filters' => [\n [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'IN',\n 'values' => $dealIds,\n ],\n ],\n ],\n ],\n 'limit' => self::MAX_FILTER_SIZE,\n ];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Psr\\Log\\LoggerInterface;\n\nclass PayloadBuilder\n{\n public const int MAX_SEARCH_REQUEST_LIMIT = 200;\n\n private const int MAX_FILTER_SIZE = 100;\n\n private const string SORT_PROPERTY = 'hs_timestamp';\n private const string ENGAGEMENT_MEETINGS = 'meetings';\n\n public function getLinkToTaskPayload(string $objectType, string $objectId, string $engagementType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($engagementType === self::ENGAGEMENT_MEETINGS) {\n $payload['filterGroups'] = $this->buildMeetingFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_timestamp',\n 'hs_activity_type',\n ];\n } else {\n $payload['filterGroups'] = $this->buildTaskFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp'];\n }\n\n return $payload;\n }\n\n public function getRecentlyUpdatedSearchPayload(Carbon $since, ?Carbon $to, array $properties): array\n {\n $lastUpdateDate = in_array('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n public function getByProfileSearchPayload(string $crmId, array $properties, Carbon $since, ?Carbon $to): array\n {\n $lastUpdateDate = array_key_exists('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n [\n 'propertyName' => 'hubspot_owner_id',\n 'operator' => 'EQ',\n 'value' => $crmId,\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n private function buildMeetingFiltersForLinkToTask($objectType, $objectId): array\n {\n $filters = [];\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'IN',\n 'values' => ['SCHEDULED', 'RESCHEDULED'],\n ],\n ],\n ];\n\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ],\n ];\n\n return $filters;\n }\n\n private function buildTaskFiltersForLinkToTask($objectType, $objectId): array\n {\n return [\n [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n ],\n ],\n ];\n }\n\n private function getAssociatedObjectFilter(string $objectType, string $objectId): array\n {\n return [\n 'propertyName' => 'associations.' . $objectType,\n 'operator' => 'EQ',\n 'value' => $objectId,\n ];\n }\n\n public function generatePlaybackURLSearchPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n public function generateGetCallsPayload(Carbon $from, Carbon $to, string $activityProvider, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $activityProvider,\n ],\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function generateSearchCallsByPeriodPayload(Carbon $from, Carbon $to, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $this->getSearchCallAttributes(),\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function getSearchCallAttributes(): array\n {\n return [\n 'hs_timestamp',\n 'hs_call_recording_url',\n 'hs_call_body',\n 'hs_call_status',\n 'hs_call_to_number',\n 'hs_call_from_number',\n 'hs_call_duration',\n 'hs_call_disposition',\n 'hs_call_title',\n 'hs_call_direction',\n 'hubspot_owner_id',\n 'hs_activity_type',\n 'hs_call_external_id',\n 'hs_call_source',\n ];\n }\n\n public function generatePlaybackAddUrlBatchPayload(array $crmUpdateData): array\n {\n $updateObjectsData = [];\n\n foreach ($crmUpdateData as $data) {\n $updateObjectsData[] = [\n 'id' => $data['crm_id'],\n 'properties' => [\n 'hs_call_body' => $data['hs_call_body'] .\n '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">' .\n '<a href=\"' . $data['playback_url'] . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>',\n ],\n ];\n }\n\n return ['inputs' => $updateObjectsData];\n }\n\n public function generateSearchCallByTokenPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload for phone search based on the specified parameters.\n *\n * @param string $phone The phone number to search.\n * @param bool $isAlternativeSearch Indicates if an alternative search should be performed\n * if the first search fails to find a match. The first search request should cover most of the cases.\n */\n public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array\n {\n $filterPropertyNames = $isAlternativeSearch ?\n ['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :\n ['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];\n\n $filterGroups = array_map(function ($propertyName) use ($phone) {\n return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);\n }, $filterPropertyNames);\n\n $properties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n\n return $this->createPayload($filterGroups, $properties);\n }\n\n /**\n * Creates a filter group with the specified parameters.\n */\n private function createFilterGroup(string $propertyName, string $operator, $value): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => $propertyName,\n 'operator' => $operator,\n 'value' => $value,\n ],\n ],\n ];\n }\n\n /**\n * Creates a payload with the specified filter groups and properties.\n */\n private function createPayload(array $filterGroups, array $properties): array\n {\n return [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $properties,\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload to find related activities based on the specified data and object type.\n * The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)\n * and are associated with any of the provided prospect types (contact or company).\n *\n * @param array $data An associative array containing the following keys:\n * - 'from': The start time for the activity search (timestamp).\n * - 'to': The end time for the activity search (timestamp).\n * - 'contact': (optional) The ID of the associated contact.\n * - 'company': (optional) The ID of the associated company.\n * @param string $objectType The type of engagement to search for (meeting or other type like call).\n *\n * @return array The payload array to be used for searching related activities.\n */\n public function getFindRelatedActivityPayload(array $data, string $objectType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($objectType === self::ENGAGEMENT_MEETINGS) {\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_activity_type',\n 'hs_timestamp',\n 'hubspot_owner_id',\n 'hs_meeting_body',\n 'hs_internal_meeting_notes',\n 'hs_meeting_location',\n 'hs_meeting_start_time',\n 'hs_meeting_end_time',\n ];\n } else {\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];\n }\n\n $timeFiltersWithEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'LTE',\n 'value' => $data['to'],\n ],\n ];\n\n $timeFiltersWithoutEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ];\n\n $payload['filterGroups'] = [];\n\n $associationTypes = ['contact', 'company'];\n foreach ($associationTypes as $type) {\n if (! empty($data[$type])) {\n $filterGroupWithEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithEndTime;\n\n $filterGroupWithoutEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithoutEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithoutEndTime ;\n }\n }\n\n return $payload;\n }\n\n /** Parameters\n * [\n * 'accountId' => $crmAccountId,\n * 'sortBy' => $sortBy,\n * 'sortDir' => $sortDir,\n * 'onlyOpen' => $onlyOpen,\n * 'closedStages' => $this->getClosedDealStages(),\n * 'userId' => $userId,\n * ];\n */\n public function generateOpportunitiesSearchPayload(\n Configuration $config,\n string $crmAccountId,\n array $closedStages,\n ): array {\n $closedFilters = [];\n $filterGroups = [];\n $onlyOpen = true;\n\n switch ($config->getOpportunityAssignmentRule()) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'ASCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n default:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n $onlyOpen = false;\n }\n\n $baseFilters = [\n [\n 'propertyName' => 'associations.company',\n 'operator' => 'EQ',\n 'value' => $crmAccountId,\n ],\n ];\n\n // Handle closed stages in chunks\n if ($onlyOpen) {\n foreach (['won', 'lost'] as $key) {\n if (! empty($closedStages[$key])) {\n $chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);\n foreach ($chunks as $chunk) {\n $closedFilters[] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $chunk,\n ];\n }\n }\n }\n }\n\n $filterGroups[] = [\n 'filters' => array_merge($baseFilters, $closedFilters),\n ];\n\n $payload = [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => $sortBy,\n 'direction' => $sortDir,\n ],\n ],\n 'properties' => [\n 'dealname',\n 'amount',\n 'hubspot_owner_id',\n 'pipeline',\n 'dealstage',\n 'closedate',\n 'deal_currency_code',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n $logger = app(LoggerInterface::class);\n $logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * Converts v1 payload data to v3\n */\n public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array\n {\n // Use this mapping until the whole Hubspot V1 is dropped.\n $properties = [\n 'hubspot_owner_id' => $engagement['ownerId'],\n 'hs_timestamp' => $engagement['timestamp'],\n ];\n\n // $engagement['activityType'] is $activity->category->name\n if (isset($engagement['activityType'])) {\n $properties['hs_activity_type'] = $engagement['activityType'];\n }\n\n $metadataKeyMap = [\n 'hs_meeting_outcome' => 'meetingOutcome',\n 'hs_meeting_title' => 'title',\n 'hs_meeting_start_time' => 'startTime',\n 'hs_meeting_end_time' => 'endTime',\n 'hs_meeting_body' => 'body',\n 'hs_internal_meeting_notes' => 'internalMeetingNotes',\n ];\n\n foreach ($metadataKeyMap as $newKey => $oldKey) {\n if (isset($metadata[$oldKey])) {\n $properties[$newKey] = $metadata[$oldKey];\n unset($metadata[$oldKey]);\n }\n }\n\n $properties = [\n ...$properties,\n ...$metadata, // custom fields\n ];\n\n $response = [\n 'properties' => $properties,\n ];\n\n if (! empty($associations)) {\n $response['associations'] = $associations;\n }\n\n return $response;\n }\n\n /**\n * Generate a payload to search for contacts by name.\n * The search is a token search in firstname or lastname\n */\n public function generateSearchContactsByNamePayload(string $name, array $fields): array\n {\n $firstNameFilter = [\n 'propertyName' => 'firstname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n $lastNameFilter = [\n 'propertyName' => 'lastname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n return [\n 'filterGroups' => [\n [\n 'filters' => [$firstNameFilter],\n ],\n [\n 'filters' => [$lastNameFilter],\n ],\n ],\n 'properties' => $fields,\n 'sorts' => [\n [\n 'propertyName' => 'lastmodifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n ];\n }\n\n public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array\n {\n $inputs = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $inputs[] = [\n 'from' => [\n 'id' => $crmId,\n ],\n 'to' => [\n 'id' => (string) $id,\n ],\n 'types' => [\n [\n 'associationCategory' => 'HUBSPOT_DEFINED',\n 'associationTypeId' => $associationType,\n ],\n ],\n ];\n }\n\n return ['inputs' => $inputs];\n }\n\n public function buildRemoveAssociationPayload(string $crmId, array $ids): array\n {\n $toArray = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $toArray[] = ['id' => (string) $id];\n }\n\n return [\n 'inputs' => [\n [\n 'from' => ['id' => $crmId],\n 'to' => $toArray,\n ],\n ],\n ];\n }\n\n public function addClosedStageFilters(array &$payload, array $closedStages): void\n {\n if (! empty($closedStages['won'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['won'],\n ];\n }\n\n if (! empty($closedStages['lost'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['lost'],\n ];\n }\n }\n\n public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void\n {\n $payload['filters'][] = [\n 'propertyName' => 'createdate',\n 'operator' => 'GT',\n 'value' => $createdAfter->getPreciseTimestamp(3),\n ];\n }\n\n public function getDealsInBulkPayload(array $dealIds): array\n {\n return [\n 'filterGroups' => [\n [\n 'filters' => [\n [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'IN',\n 'values' => $dealIds,\n ],\n ],\n ],\n ],\n 'limit' => self::MAX_FILTER_SIZE,\n ];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3653358008198671637
|
2079491755365055053
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] string $phone The phone number to search.
* @param bool $isAlternativeSearch Indicates if an alternative search should be performed
* if the first search fails to find a match. The first search request should cover most of the cases.
*/
public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array
{
$filterPropertyNames = $isAlternativeSearch ?
['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :
['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];
$filterGroups = array_map(function ($propertyName) use ($phone) {
return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);
}, $filterPropertyNames);
$properties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
return $this->createPayload($filterGroups, $properties);
}
/**
* Creates a filter group with the specified parameters.
*/
private function createFilterGroup(string $propertyName, string $operator, $value): array
{
return [
'filters' => [
[
'propertyName' => $propertyName,
'operator' => $operator,
'value' => $value,
],
],
];
}
/**
* Creates a payload with the specified filter groups and properties.
*/
private function createPayload(array $filterGroups, array $properties): array
{
return [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => 'modifieddate',
'direction' => 'DESCENDING',
],
],
'properties' => $properties,
'limit' => 1,
];
}
/**
* Generates a payload to find related activities based on the specified data and object type.
* The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)
* and are associated with any of the provided prospect types (contact or company).
*
* @param array $data An associative array containing the following keys:
* - 'from': The start time for the activity search (timestamp).
* - 'to': The end time for the activity search (timestamp).
* - 'contact': (optional) The ID of the associated contact.
* - 'company': (optional) The ID of the associated company.
* @param string $objectType The type of engagement to search for (meeting or other type like call).
*
* @return array The payload array to be used for searching related activities.
*/
public function getFindRelatedActivityPayload(array $data, string $objectType): array
{
$payload = [
'sorts' => [
[
'propertyName' => self::SORT_PROPERTY,
'direction' => 'DESCENDING',
],
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
if ($objectType === self::ENGAGEMENT_MEETINGS) {
$payload['properties'] = [
'hs_meeting_title',
'hs_meeting_outcome',
'hs_activity_type',
'hs_timestamp',
'hubspot_owner_id',
'hs_meeting_body',
'hs_internal_meeting_notes',
'hs_meeting_location',
'hs_meeting_start_time',
'hs_meeting_end_time',
];
} else {
$payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];
}
$timeFiltersWithEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'LTE',
'value' => $data['to'],
],
];
$timeFiltersWithoutEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'NOT_HAS_PROPERTY',
],
];
$payload['filterGroups'] = [];
$associationTypes = ['contact', 'company'];
foreach ($associationTypes as $type) {
if (! empty($data[$type])) {
$filterGroupWithEndTime = [
'filters' => array_merge(
$timeFiltersWithEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithEndTime;
$filterGroupWithoutEndTime = [
'filters' => array_merge(
$timeFiltersWithoutEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithoutEndTime ;
}
}
return $payload;
}
/** Parameters
* [
* 'accountId' => $crmAccountId,
* 'sortBy' => $sortBy,
* 'sortDir' => $sortDir,
* 'onlyOpen' => $onlyOpen,
* 'closedStages' => $this->getClosedDealStages(),
* 'userId' => $userId,
* ];
*/
public function generateOpportunitiesSearchPayload(
Configuration $config,
string $crmAccountId,
array $closedStages,
): array {
$closedFilters = [];
$filterGroups = [];
$onlyOpen = true;
switch ($config->getOpportunityAssignmentRule()) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$sortBy = 'createdate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$sortBy = 'createdate';
$sortDir = 'ASCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
default:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
$onlyOpen = false;
}
$baseFilters = [
[
'propertyName' => 'associations.company',
'operator' => 'EQ',
'value' => $crmAccountId,
],
];
// Handle closed stages in chunks
if ($onlyOpen) {
foreach (['won', 'lost'] as $key) {
if (! empty($closedStages[$key])) {
$chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);
foreach ($chunks as $chunk) {
$closedFilters[] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $chunk,
];
}
}
}
}
$filterGroups[] = [
'filters' => array_merge($baseFilters, $closedFilters),
];
$payload = [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => $sortBy,
'direction' => $sortDir,
],
],
'properties' => [
'dealname',
'amount',
'hubspot_owner_id',
'pipeline',
'dealstage',
'closedate',
'deal_currency_code',
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
$logger = app(LoggerInterface::class);
$logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
return $payload;
}
/**
* Converts v1 payload data to v3
*/
public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array
{
// Use this mapping until the whole Hubspot V1 is dropped.
$properties = [
'hubspot_owner_id' => $engagement['ownerId'],
'hs_timestamp' => $engagement['timestamp'],
];
// $engagement['activityType'] is $activity->category->name
if (isset($engagement['activityType'])) {
$properties['hs_activity_type'] = $engagement['activityType'];
}
$metadataKeyMap = [
'hs_meeting_outcome' => 'meetingOutcome',
'hs_meeting_title' => 'title',
'hs_meeting_start_time' => 'startTime',
'hs_meeting_end_time' => 'endTime',
'hs_meeting_body' => 'body',
'hs_internal_meeting_notes' => 'internalMeetingNotes',
];
foreach ($metadataKeyMap as $newKey => $oldKey) {
if (isset($metadata[$oldKey])) {
$properties[$newKey] = $metadata[$oldKey];
unset($metadata[$oldKey]);
}
}
$properties = [
...$properties,
...$metadata, // custom fields
];
$response = [
'properties' => $properties,
];
if (! empty($associations)) {
$response['associations'] = $associations;
}
return $response;
}
/**
* Generate a payload to search for contacts by name.
* The search is a token search in firstname or lastname
*/
public function generateSearchContactsByNamePayload(string $name, array $fields): array
{
$firstNameFilter = [
'propertyName' => 'firstname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
$lastNameFilter = [
'propertyName' => 'lastname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
return [
'filterGroups' => [
[
'filters' => [$firstNameFilter],
],
[
'filters' => [$lastNameFilter],
],
],
'properties' => $fields,
'sorts' => [
[
'propertyName' => 'lastmodifieddate',
'direction' => 'DESCENDING',
],
],
];
}
public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array
{
$inputs = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$inputs[] = [
'from' => [
'id' => $crmId,
],
'to' => [
'id' => (string) $id,
],
'types' => [
[
'associationCategory' => 'HUBSPOT_DEFINED',
'associationTypeId' => $associationType,
],
],
];
}
return ['inputs' => $inputs];
}
public function buildRemoveAssociationPayload(string $crmId, array $ids): array
{
$toArray = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$toArray[] = ['id' => (string) $id];
}
return [
'inputs' => [
[
'from' => ['id' => $crmId],
'to' => $toArray,
],
],
];
}
public function addClosedStageFilters(array &$payload, array $closedStages): void
{
if (! empty($closedStages['won'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['won'],
];
}
if (! empty($closedStages['lost'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['lost'],
];
}
}
public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void
{
$payload['filters'][] = [
'propertyName' => 'createdate',
'operator' => 'GT',
'value' => $createdAfter->getPreciseTimestamp(3),
];
}
public function getDealsInBulkPayload(array $dealIds): array
{
return [
'filterGroups' => [
[
'filters' => [
[
'propertyName' => 'hs_object_id',
'operator' => 'IN',
'values' => $dealIds,
],
],
],
],
'limit' => self::MAX_FILTER_SIZE,
];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4831
|
174
|
26
|
2026-05-07T14:31:02.754710+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164262754_m2.jpg...
|
PhpStorm
|
faVsco.js – PayloadBuilder.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] string $phone The phone number to search.
* @param bool $isAlternativeSearch Indicates if an alternative search should be performed
* if the first search fails to find a match. The first search request should cover most of the cases.
*/
public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array
{
$filterPropertyNames = $isAlternativeSearch ?
['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :
['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];
$filterGroups = array_map(function ($propertyName) use ($phone) {
return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);
}, $filterPropertyNames);
$properties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
return $this->createPayload($filterGroups, $properties);
}
/**
* Creates a filter group with the specified parameters.
*/
private function createFilterGroup(string $propertyName, string $operator, $value): array
{
return [
'filters' => [
[
'propertyName' => $propertyName,
'operator' => $operator,
'value' => $value,
],
],
];
}
/**
* Creates a payload with the specified filter groups and properties.
*/
private function createPayload(array $filterGroups, array $properties): array
{
return [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => 'modifieddate',
'direction' => 'DESCENDING',
],
],
'properties' => $properties,
'limit' => 1,
];
}
/**
* Generates a payload to find related activities based on the specified data and object type.
* The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)
* and are associated with any of the provided prospect types (contact or company).
*
* @param array $data An associative array containing the following keys:
* - 'from': The start time for the activity search (timestamp).
* - 'to': The end time for the activity search (timestamp).
* - 'contact': (optional) The ID of the associated contact.
* - 'company': (optional) The ID of the associated company.
* @param string $objectType The type of engagement to search for (meeting or other type like call).
*
* @return array The payload array to be used for searching related activities.
*/
public function getFindRelatedActivityPayload(array $data, string $objectType): array
{
$payload = [
'sorts' => [
[
'propertyName' => self::SORT_PROPERTY,
'direction' => 'DESCENDING',
],
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
if ($objectType === self::ENGAGEMENT_MEETINGS) {
$payload['properties'] = [
'hs_meeting_title',
'hs_meeting_outcome',
'hs_activity_type',
'hs_timestamp',
'hubspot_owner_id',
'hs_meeting_body',
'hs_internal_meeting_notes',
'hs_meeting_location',
'hs_meeting_start_time',
'hs_meeting_end_time',
];
} else {
$payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];
}
$timeFiltersWithEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'LTE',
'value' => $data['to'],
],
];
$timeFiltersWithoutEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'NOT_HAS_PROPERTY',
],
];
$payload['filterGroups'] = [];
$associationTypes = ['contact', 'company'];
foreach ($associationTypes as $type) {
if (! empty($data[$type])) {
$filterGroupWithEndTime = [
'filters' => array_merge(
$timeFiltersWithEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithEndTime;
$filterGroupWithoutEndTime = [
'filters' => array_merge(
$timeFiltersWithoutEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithoutEndTime ;
}
}
return $payload;
}
/** Parameters
* [
* 'accountId' => $crmAccountId,
* 'sortBy' => $sortBy,
* 'sortDir' => $sortDir,
* 'onlyOpen' => $onlyOpen,
* 'closedStages' => $this->getClosedDealStages(),
* 'userId' => $userId,
* ];
*/
public function generateOpportunitiesSearchPayload(
Configuration $config,
string $crmAccountId,
array $closedStages,
): array {
$closedFilters = [];
$filterGroups = [];
$onlyOpen = true;
switch ($config->getOpportunityAssignmentRule()) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$sortBy = 'createdate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$sortBy = 'createdate';
$sortDir = 'ASCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
default:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
$onlyOpen = false;
}
$baseFilters = [
[
'propertyName' => 'associations.company',
'operator' => 'EQ',
'value' => $crmAccountId,
],
];
// Handle closed stages in chunks
if ($onlyOpen) {
foreach (['won', 'lost'] as $key) {
if (! empty($closedStages[$key])) {
$chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);
foreach ($chunks as $chunk) {
$closedFilters[] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $chunk,
];
}
}
}
}
$filterGroups[] = [
'filters' => array_merge($baseFilters, $closedFilters),
];
$payload = [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => $sortBy,
'direction' => $sortDir,
],
],
'properties' => [
'dealname',
'amount',
'hubspot_owner_id',
'pipeline',
'dealstage',
'closedate',
'deal_currency_code',
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
$logger = app(LoggerInterface::class);
$logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
return $payload;
}
/**
* Converts v1 payload data to v3
*/
public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array
{
// Use this mapping until the whole Hubspot V1 is dropped.
$properties = [
'hubspot_owner_id' => $engagement['ownerId'],
'hs_timestamp' => $engagement['timestamp'],
];
// $engagement['activityType'] is $activity->category->name
if (isset($engagement['activityType'])) {
$properties['hs_activity_type'] = $engagement['activityType'];
}
$metadataKeyMap = [
'hs_meeting_outcome' => 'meetingOutcome',
'hs_meeting_title' => 'title',
'hs_meeting_start_time' => 'startTime',
'hs_meeting_end_time' => 'endTime',
'hs_meeting_body' => 'body',
'hs_internal_meeting_notes' => 'internalMeetingNotes',
];
foreach ($metadataKeyMap as $newKey => $oldKey) {
if (isset($metadata[$oldKey])) {
$properties[$newKey] = $metadata[$oldKey];
unset($metadata[$oldKey]);
}
}
$properties = [
...$properties,
...$metadata, // custom fields
];
$response = [
'properties' => $properties,
];
if (! empty($associations)) {
$response['associations'] = $associations;
}
return $response;
}
/**
* Generate a payload to search for contacts by name.
* The search is a token search in firstname or lastname
*/
public function generateSearchContactsByNamePayload(string $name, array $fields): array
{
$firstNameFilter = [
'propertyName' => 'firstname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
$lastNameFilter = [
'propertyName' => 'lastname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
return [
'filterGroups' => [
[
'filters' => [$firstNameFilter],
],
[
'filters' => [$lastNameFilter],
],
],
'properties' => $fields,
'sorts' => [
[
'propertyName' => 'lastmodifieddate',
'direction' => 'DESCENDING',
],
],
];
}
public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array
{
$inputs = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$inputs[] = [
'from' => [
'id' => $crmId,
],
'to' => [
'id' => (string) $id,
],
'types' => [
[
'associationCategory' => 'HUBSPOT_DEFINED',
'associationTypeId' => $associationType,
],
],
];
}
return ['inputs' => $inputs];
}
public function buildRemoveAssociationPayload(string $crmId, array $ids): array
{
$toArray = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$toArray[] = ['id' => (string) $id];
}
return [
'inputs' => [
[
'from' => ['id' => $crmId],
'to' => $toArray,
],
],
];
}
public function addClosedStageFilters(array &$payload, array $closedStages): void
{
if (! empty($closedStages['won'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['won'],
];
}
if (! empty($closedStages['lost'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['lost'],
];
}
}
public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void
{
$payload['filters'][] = [
'propertyName' => 'createdate',
'operator' => 'GT',
'value' => $createdAfter->getPreciseTimestamp(3),
];
}
public function getDealsInBulkPayload(array $dealIds): array
{
return [
'filterGroups' => [
[
'filters' => [
[
'propertyName' => 'hs_object_id',
'operator' => 'IN',
'values' => $dealIds,
],
],
],
],
'limit' => self::MAX_FILTER_SIZE,
];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.39660904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.40525267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4162234,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.42486703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.43351063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.44448137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4554521,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.4820479,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.49301863,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.6821808,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.66855055,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.67852396,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6878325,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6974734,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.70478725,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.35904256,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"25","depth":4,"bounds":{"left":0.36901596,"top":0.2490024,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.3806516,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.3879654,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Psr\\Log\\LoggerInterface;\n\nclass PayloadBuilder\n{\n public const int MAX_SEARCH_REQUEST_LIMIT = 200;\n\n private const int MAX_FILTER_SIZE = 100;\n\n private const string SORT_PROPERTY = 'hs_timestamp';\n private const string ENGAGEMENT_MEETINGS = 'meetings';\n\n public function getLinkToTaskPayload(string $objectType, string $objectId, string $engagementType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($engagementType === self::ENGAGEMENT_MEETINGS) {\n $payload['filterGroups'] = $this->buildMeetingFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_timestamp',\n 'hs_activity_type',\n ];\n } else {\n $payload['filterGroups'] = $this->buildTaskFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp'];\n }\n\n return $payload;\n }\n\n public function getRecentlyUpdatedSearchPayload(Carbon $since, ?Carbon $to, array $properties): array\n {\n $lastUpdateDate = in_array('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n public function getByProfileSearchPayload(string $crmId, array $properties, Carbon $since, ?Carbon $to): array\n {\n $lastUpdateDate = array_key_exists('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n [\n 'propertyName' => 'hubspot_owner_id',\n 'operator' => 'EQ',\n 'value' => $crmId,\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n private function buildMeetingFiltersForLinkToTask($objectType, $objectId): array\n {\n $filters = [];\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'IN',\n 'values' => ['SCHEDULED', 'RESCHEDULED'],\n ],\n ],\n ];\n\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ],\n ];\n\n return $filters;\n }\n\n private function buildTaskFiltersForLinkToTask($objectType, $objectId): array\n {\n return [\n [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n ],\n ],\n ];\n }\n\n private function getAssociatedObjectFilter(string $objectType, string $objectId): array\n {\n return [\n 'propertyName' => 'associations.' . $objectType,\n 'operator' => 'EQ',\n 'value' => $objectId,\n ];\n }\n\n public function generatePlaybackURLSearchPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n public function generateGetCallsPayload(Carbon $from, Carbon $to, string $activityProvider, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $activityProvider,\n ],\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function generateSearchCallsByPeriodPayload(Carbon $from, Carbon $to, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $this->getSearchCallAttributes(),\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function getSearchCallAttributes(): array\n {\n return [\n 'hs_timestamp',\n 'hs_call_recording_url',\n 'hs_call_body',\n 'hs_call_status',\n 'hs_call_to_number',\n 'hs_call_from_number',\n 'hs_call_duration',\n 'hs_call_disposition',\n 'hs_call_title',\n 'hs_call_direction',\n 'hubspot_owner_id',\n 'hs_activity_type',\n 'hs_call_external_id',\n 'hs_call_source',\n ];\n }\n\n public function generatePlaybackAddUrlBatchPayload(array $crmUpdateData): array\n {\n $updateObjectsData = [];\n\n foreach ($crmUpdateData as $data) {\n $updateObjectsData[] = [\n 'id' => $data['crm_id'],\n 'properties' => [\n 'hs_call_body' => $data['hs_call_body'] .\n '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">' .\n '<a href=\"' . $data['playback_url'] . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>',\n ],\n ];\n }\n\n return ['inputs' => $updateObjectsData];\n }\n\n public function generateSearchCallByTokenPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload for phone search based on the specified parameters.\n *\n * @param string $phone The phone number to search.\n * @param bool $isAlternativeSearch Indicates if an alternative search should be performed\n * if the first search fails to find a match. The first search request should cover most of the cases.\n */\n public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array\n {\n $filterPropertyNames = $isAlternativeSearch ?\n ['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :\n ['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];\n\n $filterGroups = array_map(function ($propertyName) use ($phone) {\n return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);\n }, $filterPropertyNames);\n\n $properties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n\n return $this->createPayload($filterGroups, $properties);\n }\n\n /**\n * Creates a filter group with the specified parameters.\n */\n private function createFilterGroup(string $propertyName, string $operator, $value): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => $propertyName,\n 'operator' => $operator,\n 'value' => $value,\n ],\n ],\n ];\n }\n\n /**\n * Creates a payload with the specified filter groups and properties.\n */\n private function createPayload(array $filterGroups, array $properties): array\n {\n return [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $properties,\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload to find related activities based on the specified data and object type.\n * The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)\n * and are associated with any of the provided prospect types (contact or company).\n *\n * @param array $data An associative array containing the following keys:\n * - 'from': The start time for the activity search (timestamp).\n * - 'to': The end time for the activity search (timestamp).\n * - 'contact': (optional) The ID of the associated contact.\n * - 'company': (optional) The ID of the associated company.\n * @param string $objectType The type of engagement to search for (meeting or other type like call).\n *\n * @return array The payload array to be used for searching related activities.\n */\n public function getFindRelatedActivityPayload(array $data, string $objectType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($objectType === self::ENGAGEMENT_MEETINGS) {\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_activity_type',\n 'hs_timestamp',\n 'hubspot_owner_id',\n 'hs_meeting_body',\n 'hs_internal_meeting_notes',\n 'hs_meeting_location',\n 'hs_meeting_start_time',\n 'hs_meeting_end_time',\n ];\n } else {\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];\n }\n\n $timeFiltersWithEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'LTE',\n 'value' => $data['to'],\n ],\n ];\n\n $timeFiltersWithoutEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ];\n\n $payload['filterGroups'] = [];\n\n $associationTypes = ['contact', 'company'];\n foreach ($associationTypes as $type) {\n if (! empty($data[$type])) {\n $filterGroupWithEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithEndTime;\n\n $filterGroupWithoutEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithoutEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithoutEndTime ;\n }\n }\n\n return $payload;\n }\n\n /** Parameters\n * [\n * 'accountId' => $crmAccountId,\n * 'sortBy' => $sortBy,\n * 'sortDir' => $sortDir,\n * 'onlyOpen' => $onlyOpen,\n * 'closedStages' => $this->getClosedDealStages(),\n * 'userId' => $userId,\n * ];\n */\n public function generateOpportunitiesSearchPayload(\n Configuration $config,\n string $crmAccountId,\n array $closedStages,\n ): array {\n $closedFilters = [];\n $filterGroups = [];\n $onlyOpen = true;\n\n switch ($config->getOpportunityAssignmentRule()) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'ASCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n default:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n $onlyOpen = false;\n }\n\n $baseFilters = [\n [\n 'propertyName' => 'associations.company',\n 'operator' => 'EQ',\n 'value' => $crmAccountId,\n ],\n ];\n\n // Handle closed stages in chunks\n if ($onlyOpen) {\n foreach (['won', 'lost'] as $key) {\n if (! empty($closedStages[$key])) {\n $chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);\n foreach ($chunks as $chunk) {\n $closedFilters[] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $chunk,\n ];\n }\n }\n }\n }\n\n $filterGroups[] = [\n 'filters' => array_merge($baseFilters, $closedFilters),\n ];\n\n $payload = [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => $sortBy,\n 'direction' => $sortDir,\n ],\n ],\n 'properties' => [\n 'dealname',\n 'amount',\n 'hubspot_owner_id',\n 'pipeline',\n 'dealstage',\n 'closedate',\n 'deal_currency_code',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n $logger = app(LoggerInterface::class);\n $logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * Converts v1 payload data to v3\n */\n public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array\n {\n // Use this mapping until the whole Hubspot V1 is dropped.\n $properties = [\n 'hubspot_owner_id' => $engagement['ownerId'],\n 'hs_timestamp' => $engagement['timestamp'],\n ];\n\n // $engagement['activityType'] is $activity->category->name\n if (isset($engagement['activityType'])) {\n $properties['hs_activity_type'] = $engagement['activityType'];\n }\n\n $metadataKeyMap = [\n 'hs_meeting_outcome' => 'meetingOutcome',\n 'hs_meeting_title' => 'title',\n 'hs_meeting_start_time' => 'startTime',\n 'hs_meeting_end_time' => 'endTime',\n 'hs_meeting_body' => 'body',\n 'hs_internal_meeting_notes' => 'internalMeetingNotes',\n ];\n\n foreach ($metadataKeyMap as $newKey => $oldKey) {\n if (isset($metadata[$oldKey])) {\n $properties[$newKey] = $metadata[$oldKey];\n unset($metadata[$oldKey]);\n }\n }\n\n $properties = [\n ...$properties,\n ...$metadata, // custom fields\n ];\n\n $response = [\n 'properties' => $properties,\n ];\n\n if (! empty($associations)) {\n $response['associations'] = $associations;\n }\n\n return $response;\n }\n\n /**\n * Generate a payload to search for contacts by name.\n * The search is a token search in firstname or lastname\n */\n public function generateSearchContactsByNamePayload(string $name, array $fields): array\n {\n $firstNameFilter = [\n 'propertyName' => 'firstname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n $lastNameFilter = [\n 'propertyName' => 'lastname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n return [\n 'filterGroups' => [\n [\n 'filters' => [$firstNameFilter],\n ],\n [\n 'filters' => [$lastNameFilter],\n ],\n ],\n 'properties' => $fields,\n 'sorts' => [\n [\n 'propertyName' => 'lastmodifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n ];\n }\n\n public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array\n {\n $inputs = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $inputs[] = [\n 'from' => [\n 'id' => $crmId,\n ],\n 'to' => [\n 'id' => (string) $id,\n ],\n 'types' => [\n [\n 'associationCategory' => 'HUBSPOT_DEFINED',\n 'associationTypeId' => $associationType,\n ],\n ],\n ];\n }\n\n return ['inputs' => $inputs];\n }\n\n public function buildRemoveAssociationPayload(string $crmId, array $ids): array\n {\n $toArray = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $toArray[] = ['id' => (string) $id];\n }\n\n return [\n 'inputs' => [\n [\n 'from' => ['id' => $crmId],\n 'to' => $toArray,\n ],\n ],\n ];\n }\n\n public function addClosedStageFilters(array &$payload, array $closedStages): void\n {\n if (! empty($closedStages['won'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['won'],\n ];\n }\n\n if (! empty($closedStages['lost'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['lost'],\n ];\n }\n }\n\n public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void\n {\n $payload['filters'][] = [\n 'propertyName' => 'createdate',\n 'operator' => 'GT',\n 'value' => $createdAfter->getPreciseTimestamp(3),\n ];\n }\n\n public function getDealsInBulkPayload(array $dealIds): array\n {\n return [\n 'filterGroups' => [\n [\n 'filters' => [\n [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'IN',\n 'values' => $dealIds,\n ],\n ],\n ],\n ],\n 'limit' => self::MAX_FILTER_SIZE,\n ];\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.24581006,"width":0.35738033,"height":0.75418997},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Psr\\Log\\LoggerInterface;\n\nclass PayloadBuilder\n{\n public const int MAX_SEARCH_REQUEST_LIMIT = 200;\n\n private const int MAX_FILTER_SIZE = 100;\n\n private const string SORT_PROPERTY = 'hs_timestamp';\n private const string ENGAGEMENT_MEETINGS = 'meetings';\n\n public function getLinkToTaskPayload(string $objectType, string $objectId, string $engagementType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($engagementType === self::ENGAGEMENT_MEETINGS) {\n $payload['filterGroups'] = $this->buildMeetingFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_timestamp',\n 'hs_activity_type',\n ];\n } else {\n $payload['filterGroups'] = $this->buildTaskFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp'];\n }\n\n return $payload;\n }\n\n public function getRecentlyUpdatedSearchPayload(Carbon $since, ?Carbon $to, array $properties): array\n {\n $lastUpdateDate = in_array('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n public function getByProfileSearchPayload(string $crmId, array $properties, Carbon $since, ?Carbon $to): array\n {\n $lastUpdateDate = array_key_exists('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n [\n 'propertyName' => 'hubspot_owner_id',\n 'operator' => 'EQ',\n 'value' => $crmId,\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n private function buildMeetingFiltersForLinkToTask($objectType, $objectId): array\n {\n $filters = [];\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'IN',\n 'values' => ['SCHEDULED', 'RESCHEDULED'],\n ],\n ],\n ];\n\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ],\n ];\n\n return $filters;\n }\n\n private function buildTaskFiltersForLinkToTask($objectType, $objectId): array\n {\n return [\n [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n ],\n ],\n ];\n }\n\n private function getAssociatedObjectFilter(string $objectType, string $objectId): array\n {\n return [\n 'propertyName' => 'associations.' . $objectType,\n 'operator' => 'EQ',\n 'value' => $objectId,\n ];\n }\n\n public function generatePlaybackURLSearchPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n public function generateGetCallsPayload(Carbon $from, Carbon $to, string $activityProvider, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $activityProvider,\n ],\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function generateSearchCallsByPeriodPayload(Carbon $from, Carbon $to, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $this->getSearchCallAttributes(),\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function getSearchCallAttributes(): array\n {\n return [\n 'hs_timestamp',\n 'hs_call_recording_url',\n 'hs_call_body',\n 'hs_call_status',\n 'hs_call_to_number',\n 'hs_call_from_number',\n 'hs_call_duration',\n 'hs_call_disposition',\n 'hs_call_title',\n 'hs_call_direction',\n 'hubspot_owner_id',\n 'hs_activity_type',\n 'hs_call_external_id',\n 'hs_call_source',\n ];\n }\n\n public function generatePlaybackAddUrlBatchPayload(array $crmUpdateData): array\n {\n $updateObjectsData = [];\n\n foreach ($crmUpdateData as $data) {\n $updateObjectsData[] = [\n 'id' => $data['crm_id'],\n 'properties' => [\n 'hs_call_body' => $data['hs_call_body'] .\n '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">' .\n '<a href=\"' . $data['playback_url'] . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>',\n ],\n ];\n }\n\n return ['inputs' => $updateObjectsData];\n }\n\n public function generateSearchCallByTokenPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload for phone search based on the specified parameters.\n *\n * @param string $phone The phone number to search.\n * @param bool $isAlternativeSearch Indicates if an alternative search should be performed\n * if the first search fails to find a match. The first search request should cover most of the cases.\n */\n public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array\n {\n $filterPropertyNames = $isAlternativeSearch ?\n ['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :\n ['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];\n\n $filterGroups = array_map(function ($propertyName) use ($phone) {\n return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);\n }, $filterPropertyNames);\n\n $properties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n\n return $this->createPayload($filterGroups, $properties);\n }\n\n /**\n * Creates a filter group with the specified parameters.\n */\n private function createFilterGroup(string $propertyName, string $operator, $value): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => $propertyName,\n 'operator' => $operator,\n 'value' => $value,\n ],\n ],\n ];\n }\n\n /**\n * Creates a payload with the specified filter groups and properties.\n */\n private function createPayload(array $filterGroups, array $properties): array\n {\n return [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $properties,\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload to find related activities based on the specified data and object type.\n * The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)\n * and are associated with any of the provided prospect types (contact or company).\n *\n * @param array $data An associative array containing the following keys:\n * - 'from': The start time for the activity search (timestamp).\n * - 'to': The end time for the activity search (timestamp).\n * - 'contact': (optional) The ID of the associated contact.\n * - 'company': (optional) The ID of the associated company.\n * @param string $objectType The type of engagement to search for (meeting or other type like call).\n *\n * @return array The payload array to be used for searching related activities.\n */\n public function getFindRelatedActivityPayload(array $data, string $objectType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($objectType === self::ENGAGEMENT_MEETINGS) {\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_activity_type',\n 'hs_timestamp',\n 'hubspot_owner_id',\n 'hs_meeting_body',\n 'hs_internal_meeting_notes',\n 'hs_meeting_location',\n 'hs_meeting_start_time',\n 'hs_meeting_end_time',\n ];\n } else {\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];\n }\n\n $timeFiltersWithEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'LTE',\n 'value' => $data['to'],\n ],\n ];\n\n $timeFiltersWithoutEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ];\n\n $payload['filterGroups'] = [];\n\n $associationTypes = ['contact', 'company'];\n foreach ($associationTypes as $type) {\n if (! empty($data[$type])) {\n $filterGroupWithEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithEndTime;\n\n $filterGroupWithoutEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithoutEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithoutEndTime ;\n }\n }\n\n return $payload;\n }\n\n /** Parameters\n * [\n * 'accountId' => $crmAccountId,\n * 'sortBy' => $sortBy,\n * 'sortDir' => $sortDir,\n * 'onlyOpen' => $onlyOpen,\n * 'closedStages' => $this->getClosedDealStages(),\n * 'userId' => $userId,\n * ];\n */\n public function generateOpportunitiesSearchPayload(\n Configuration $config,\n string $crmAccountId,\n array $closedStages,\n ): array {\n $closedFilters = [];\n $filterGroups = [];\n $onlyOpen = true;\n\n switch ($config->getOpportunityAssignmentRule()) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'ASCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n default:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n $onlyOpen = false;\n }\n\n $baseFilters = [\n [\n 'propertyName' => 'associations.company',\n 'operator' => 'EQ',\n 'value' => $crmAccountId,\n ],\n ];\n\n // Handle closed stages in chunks\n if ($onlyOpen) {\n foreach (['won', 'lost'] as $key) {\n if (! empty($closedStages[$key])) {\n $chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);\n foreach ($chunks as $chunk) {\n $closedFilters[] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $chunk,\n ];\n }\n }\n }\n }\n\n $filterGroups[] = [\n 'filters' => array_merge($baseFilters, $closedFilters),\n ];\n\n $payload = [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => $sortBy,\n 'direction' => $sortDir,\n ],\n ],\n 'properties' => [\n 'dealname',\n 'amount',\n 'hubspot_owner_id',\n 'pipeline',\n 'dealstage',\n 'closedate',\n 'deal_currency_code',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n $logger = app(LoggerInterface::class);\n $logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * Converts v1 payload data to v3\n */\n public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array\n {\n // Use this mapping until the whole Hubspot V1 is dropped.\n $properties = [\n 'hubspot_owner_id' => $engagement['ownerId'],\n 'hs_timestamp' => $engagement['timestamp'],\n ];\n\n // $engagement['activityType'] is $activity->category->name\n if (isset($engagement['activityType'])) {\n $properties['hs_activity_type'] = $engagement['activityType'];\n }\n\n $metadataKeyMap = [\n 'hs_meeting_outcome' => 'meetingOutcome',\n 'hs_meeting_title' => 'title',\n 'hs_meeting_start_time' => 'startTime',\n 'hs_meeting_end_time' => 'endTime',\n 'hs_meeting_body' => 'body',\n 'hs_internal_meeting_notes' => 'internalMeetingNotes',\n ];\n\n foreach ($metadataKeyMap as $newKey => $oldKey) {\n if (isset($metadata[$oldKey])) {\n $properties[$newKey] = $metadata[$oldKey];\n unset($metadata[$oldKey]);\n }\n }\n\n $properties = [\n ...$properties,\n ...$metadata, // custom fields\n ];\n\n $response = [\n 'properties' => $properties,\n ];\n\n if (! empty($associations)) {\n $response['associations'] = $associations;\n }\n\n return $response;\n }\n\n /**\n * Generate a payload to search for contacts by name.\n * The search is a token search in firstname or lastname\n */\n public function generateSearchContactsByNamePayload(string $name, array $fields): array\n {\n $firstNameFilter = [\n 'propertyName' => 'firstname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n $lastNameFilter = [\n 'propertyName' => 'lastname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n return [\n 'filterGroups' => [\n [\n 'filters' => [$firstNameFilter],\n ],\n [\n 'filters' => [$lastNameFilter],\n ],\n ],\n 'properties' => $fields,\n 'sorts' => [\n [\n 'propertyName' => 'lastmodifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n ];\n }\n\n public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array\n {\n $inputs = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $inputs[] = [\n 'from' => [\n 'id' => $crmId,\n ],\n 'to' => [\n 'id' => (string) $id,\n ],\n 'types' => [\n [\n 'associationCategory' => 'HUBSPOT_DEFINED',\n 'associationTypeId' => $associationType,\n ],\n ],\n ];\n }\n\n return ['inputs' => $inputs];\n }\n\n public function buildRemoveAssociationPayload(string $crmId, array $ids): array\n {\n $toArray = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $toArray[] = ['id' => (string) $id];\n }\n\n return [\n 'inputs' => [\n [\n 'from' => ['id' => $crmId],\n 'to' => $toArray,\n ],\n ],\n ];\n }\n\n public function addClosedStageFilters(array &$payload, array $closedStages): void\n {\n if (! empty($closedStages['won'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['won'],\n ];\n }\n\n if (! empty($closedStages['lost'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['lost'],\n ];\n }\n }\n\n public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void\n {\n $payload['filters'][] = [\n 'propertyName' => 'createdate',\n 'operator' => 'GT',\n 'value' => $createdAfter->getPreciseTimestamp(3),\n ];\n }\n\n public function getDealsInBulkPayload(array $dealIds): array\n {\n return [\n 'filterGroups' => [\n [\n 'filters' => [\n [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'IN',\n 'values' => $dealIds,\n ],\n ],\n ],\n ],\n 'limit' => self::MAX_FILTER_SIZE,\n ];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3653358008198671637
|
2079491755365055053
|
visual_change
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] string $phone The phone number to search.
* @param bool $isAlternativeSearch Indicates if an alternative search should be performed
* if the first search fails to find a match. The first search request should cover most of the cases.
*/
public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array
{
$filterPropertyNames = $isAlternativeSearch ?
['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :
['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];
$filterGroups = array_map(function ($propertyName) use ($phone) {
return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);
}, $filterPropertyNames);
$properties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
return $this->createPayload($filterGroups, $properties);
}
/**
* Creates a filter group with the specified parameters.
*/
private function createFilterGroup(string $propertyName, string $operator, $value): array
{
return [
'filters' => [
[
'propertyName' => $propertyName,
'operator' => $operator,
'value' => $value,
],
],
];
}
/**
* Creates a payload with the specified filter groups and properties.
*/
private function createPayload(array $filterGroups, array $properties): array
{
return [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => 'modifieddate',
'direction' => 'DESCENDING',
],
],
'properties' => $properties,
'limit' => 1,
];
}
/**
* Generates a payload to find related activities based on the specified data and object type.
* The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)
* and are associated with any of the provided prospect types (contact or company).
*
* @param array $data An associative array containing the following keys:
* - 'from': The start time for the activity search (timestamp).
* - 'to': The end time for the activity search (timestamp).
* - 'contact': (optional) The ID of the associated contact.
* - 'company': (optional) The ID of the associated company.
* @param string $objectType The type of engagement to search for (meeting or other type like call).
*
* @return array The payload array to be used for searching related activities.
*/
public function getFindRelatedActivityPayload(array $data, string $objectType): array
{
$payload = [
'sorts' => [
[
'propertyName' => self::SORT_PROPERTY,
'direction' => 'DESCENDING',
],
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
if ($objectType === self::ENGAGEMENT_MEETINGS) {
$payload['properties'] = [
'hs_meeting_title',
'hs_meeting_outcome',
'hs_activity_type',
'hs_timestamp',
'hubspot_owner_id',
'hs_meeting_body',
'hs_internal_meeting_notes',
'hs_meeting_location',
'hs_meeting_start_time',
'hs_meeting_end_time',
];
} else {
$payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];
}
$timeFiltersWithEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'LTE',
'value' => $data['to'],
],
];
$timeFiltersWithoutEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'NOT_HAS_PROPERTY',
],
];
$payload['filterGroups'] = [];
$associationTypes = ['contact', 'company'];
foreach ($associationTypes as $type) {
if (! empty($data[$type])) {
$filterGroupWithEndTime = [
'filters' => array_merge(
$timeFiltersWithEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithEndTime;
$filterGroupWithoutEndTime = [
'filters' => array_merge(
$timeFiltersWithoutEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithoutEndTime ;
}
}
return $payload;
}
/** Parameters
* [
* 'accountId' => $crmAccountId,
* 'sortBy' => $sortBy,
* 'sortDir' => $sortDir,
* 'onlyOpen' => $onlyOpen,
* 'closedStages' => $this->getClosedDealStages(),
* 'userId' => $userId,
* ];
*/
public function generateOpportunitiesSearchPayload(
Configuration $config,
string $crmAccountId,
array $closedStages,
): array {
$closedFilters = [];
$filterGroups = [];
$onlyOpen = true;
switch ($config->getOpportunityAssignmentRule()) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$sortBy = 'createdate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$sortBy = 'createdate';
$sortDir = 'ASCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
default:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
$onlyOpen = false;
}
$baseFilters = [
[
'propertyName' => 'associations.company',
'operator' => 'EQ',
'value' => $crmAccountId,
],
];
// Handle closed stages in chunks
if ($onlyOpen) {
foreach (['won', 'lost'] as $key) {
if (! empty($closedStages[$key])) {
$chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);
foreach ($chunks as $chunk) {
$closedFilters[] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $chunk,
];
}
}
}
}
$filterGroups[] = [
'filters' => array_merge($baseFilters, $closedFilters),
];
$payload = [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => $sortBy,
'direction' => $sortDir,
],
],
'properties' => [
'dealname',
'amount',
'hubspot_owner_id',
'pipeline',
'dealstage',
'closedate',
'deal_currency_code',
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
$logger = app(LoggerInterface::class);
$logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
return $payload;
}
/**
* Converts v1 payload data to v3
*/
public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array
{
// Use this mapping until the whole Hubspot V1 is dropped.
$properties = [
'hubspot_owner_id' => $engagement['ownerId'],
'hs_timestamp' => $engagement['timestamp'],
];
// $engagement['activityType'] is $activity->category->name
if (isset($engagement['activityType'])) {
$properties['hs_activity_type'] = $engagement['activityType'];
}
$metadataKeyMap = [
'hs_meeting_outcome' => 'meetingOutcome',
'hs_meeting_title' => 'title',
'hs_meeting_start_time' => 'startTime',
'hs_meeting_end_time' => 'endTime',
'hs_meeting_body' => 'body',
'hs_internal_meeting_notes' => 'internalMeetingNotes',
];
foreach ($metadataKeyMap as $newKey => $oldKey) {
if (isset($metadata[$oldKey])) {
$properties[$newKey] = $metadata[$oldKey];
unset($metadata[$oldKey]);
}
}
$properties = [
...$properties,
...$metadata, // custom fields
];
$response = [
'properties' => $properties,
];
if (! empty($associations)) {
$response['associations'] = $associations;
}
return $response;
}
/**
* Generate a payload to search for contacts by name.
* The search is a token search in firstname or lastname
*/
public function generateSearchContactsByNamePayload(string $name, array $fields): array
{
$firstNameFilter = [
'propertyName' => 'firstname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
$lastNameFilter = [
'propertyName' => 'lastname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
return [
'filterGroups' => [
[
'filters' => [$firstNameFilter],
],
[
'filters' => [$lastNameFilter],
],
],
'properties' => $fields,
'sorts' => [
[
'propertyName' => 'lastmodifieddate',
'direction' => 'DESCENDING',
],
],
];
}
public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array
{
$inputs = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$inputs[] = [
'from' => [
'id' => $crmId,
],
'to' => [
'id' => (string) $id,
],
'types' => [
[
'associationCategory' => 'HUBSPOT_DEFINED',
'associationTypeId' => $associationType,
],
],
];
}
return ['inputs' => $inputs];
}
public function buildRemoveAssociationPayload(string $crmId, array $ids): array
{
$toArray = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$toArray[] = ['id' => (string) $id];
}
return [
'inputs' => [
[
'from' => ['id' => $crmId],
'to' => $toArray,
],
],
];
}
public function addClosedStageFilters(array &$payload, array $closedStages): void
{
if (! empty($closedStages['won'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['won'],
];
}
if (! empty($closedStages['lost'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['lost'],
];
}
}
public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void
{
$payload['filters'][] = [
'propertyName' => 'createdate',
'operator' => 'GT',
'value' => $createdAfter->getPreciseTimestamp(3),
];
}
public function getDealsInBulkPayload(array $dealIds): array
{
return [
'filterGroups' => [
[
'filters' => [
[
'propertyName' => 'hs_object_id',
'operator' => 'IN',
'values' => $dealIds,
],
],
],
],
'limit' => self::MAX_FILTER_SIZE,
];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4832
|
174
|
27
|
2026-05-07T14:31:24.091755+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164284091_m2.jpg...
|
PhpStorm
|
faVsco.js – PayloadBuilder.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] string $phone The phone number to search.
* @param bool $isAlternativeSearch Indicates if an alternative search should be performed
* if the first search fails to find a match. The first search request should cover most of the cases.
*/
public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array
{
$filterPropertyNames = $isAlternativeSearch ?
['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :
['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];
$filterGroups = array_map(function ($propertyName) use ($phone) {
return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);
}, $filterPropertyNames);
$properties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
return $this->createPayload($filterGroups, $properties);
}
/**
* Creates a filter group with the specified parameters.
*/
private function createFilterGroup(string $propertyName, string $operator, $value): array
{
return [
'filters' => [
[
'propertyName' => $propertyName,
'operator' => $operator,
'value' => $value,
],
],
];
}
/**
* Creates a payload with the specified filter groups and properties.
*/
private function createPayload(array $filterGroups, array $properties): array
{
return [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => 'modifieddate',
'direction' => 'DESCENDING',
],
],
'properties' => $properties,
'limit' => 1,
];
}
/**
* Generates a payload to find related activities based on the specified data and object type.
* The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)
* and are associated with any of the provided prospect types (contact or company).
*
* @param array $data An associative array containing the following keys:
* - 'from': The start time for the activity search (timestamp).
* - 'to': The end time for the activity search (timestamp).
* - 'contact': (optional) The ID of the associated contact.
* - 'company': (optional) The ID of the associated company.
* @param string $objectType The type of engagement to search for (meeting or other type like call).
*
* @return array The payload array to be used for searching related activities.
*/
public function getFindRelatedActivityPayload(array $data, string $objectType): array
{
$payload = [
'sorts' => [
[
'propertyName' => self::SORT_PROPERTY,
'direction' => 'DESCENDING',
],
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
if ($objectType === self::ENGAGEMENT_MEETINGS) {
$payload['properties'] = [
'hs_meeting_title',
'hs_meeting_outcome',
'hs_activity_type',
'hs_timestamp',
'hubspot_owner_id',
'hs_meeting_body',
'hs_internal_meeting_notes',
'hs_meeting_location',
'hs_meeting_start_time',
'hs_meeting_end_time',
];
} else {
$payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];
}
$timeFiltersWithEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'LTE',
'value' => $data['to'],
],
];
$timeFiltersWithoutEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'NOT_HAS_PROPERTY',
],
];
$payload['filterGroups'] = [];
$associationTypes = ['contact', 'company'];
foreach ($associationTypes as $type) {
if (! empty($data[$type])) {
$filterGroupWithEndTime = [
'filters' => array_merge(
$timeFiltersWithEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithEndTime;
$filterGroupWithoutEndTime = [
'filters' => array_merge(
$timeFiltersWithoutEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithoutEndTime ;
}
}
return $payload;
}
/** Parameters
* [
* 'accountId' => $crmAccountId,
* 'sortBy' => $sortBy,
* 'sortDir' => $sortDir,
* 'onlyOpen' => $onlyOpen,
* 'closedStages' => $this->getClosedDealStages(),
* 'userId' => $userId,
* ];
*/
public function generateOpportunitiesSearchPayload(
Configuration $config,
string $crmAccountId,
array $closedStages,
): array {
$closedFilters = [];
$filterGroups = [];
$onlyOpen = true;
switch ($config->getOpportunityAssignmentRule()) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$sortBy = 'createdate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$sortBy = 'createdate';
$sortDir = 'ASCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
default:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
$onlyOpen = false;
}
$baseFilters = [
[
'propertyName' => 'associations.company',
'operator' => 'EQ',
'value' => $crmAccountId,
],
];
// Handle closed stages in chunks
if ($onlyOpen) {
foreach (['won', 'lost'] as $key) {
if (! empty($closedStages[$key])) {
$chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);
foreach ($chunks as $chunk) {
$closedFilters[] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $chunk,
];
}
}
}
}
$filterGroups[] = [
'filters' => array_merge($baseFilters, $closedFilters),
];
$payload = [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => $sortBy,
'direction' => $sortDir,
],
],
'properties' => [
'dealname',
'amount',
'hubspot_owner_id',
'pipeline',
'dealstage',
'closedate',
'deal_currency_code',
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
$logger = app(LoggerInterface::class);
$logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
return $payload;
}
/**
* Converts v1 payload data to v3
*/
public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array
{
// Use this mapping until the whole Hubspot V1 is dropped.
$properties = [
'hubspot_owner_id' => $engagement['ownerId'],
'hs_timestamp' => $engagement['timestamp'],
];
// $engagement['activityType'] is $activity->category->name
if (isset($engagement['activityType'])) {
$properties['hs_activity_type'] = $engagement['activityType'];
}
$metadataKeyMap = [
'hs_meeting_outcome' => 'meetingOutcome',
'hs_meeting_title' => 'title',
'hs_meeting_start_time' => 'startTime',
'hs_meeting_end_time' => 'endTime',
'hs_meeting_body' => 'body',
'hs_internal_meeting_notes' => 'internalMeetingNotes',
];
foreach ($metadataKeyMap as $newKey => $oldKey) {
if (isset($metadata[$oldKey])) {
$properties[$newKey] = $metadata[$oldKey];
unset($metadata[$oldKey]);
}
}
$properties = [
...$properties,
...$metadata, // custom fields
];
$response = [
'properties' => $properties,
];
if (! empty($associations)) {
$response['associations'] = $associations;
}
return $response;
}
/**
* Generate a payload to search for contacts by name.
* The search is a token search in firstname or lastname
*/
public function generateSearchContactsByNamePayload(string $name, array $fields): array
{
$firstNameFilter = [
'propertyName' => 'firstname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
$lastNameFilter = [
'propertyName' => 'lastname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
return [
'filterGroups' => [
[
'filters' => [$firstNameFilter],
],
[
'filters' => [$lastNameFilter],
],
],
'properties' => $fields,
'sorts' => [
[
'propertyName' => 'lastmodifieddate',
'direction' => 'DESCENDING',
],
],
];
}
public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array
{
$inputs = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$inputs[] = [
'from' => [
'id' => $crmId,
],
'to' => [
'id' => (string) $id,
],
'types' => [
[
'associationCategory' => 'HUBSPOT_DEFINED',
'associationTypeId' => $associationType,
],
],
];
}
return ['inputs' => $inputs];
}
public function buildRemoveAssociationPayload(string $crmId, array $ids): array
{
$toArray = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$toArray[] = ['id' => (string) $id];
}
return [
'inputs' => [
[
'from' => ['id' => $crmId],
'to' => $toArray,
],
],
];
}
public function addClosedStageFilters(array &$payload, array $closedStages): void
{
if (! empty($closedStages['won'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['won'],
];
}
if (! empty($closedStages['lost'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['lost'],
];
}
}
public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void
{
$payload['filters'][] = [
'propertyName' => 'createdate',
'operator' => 'GT',
'value' => $createdAfter->getPreciseTimestamp(3),
];
}
public function getDealsInBulkPayload(array $dealIds): array
{
return [
'filterGroups' => [
[
'filters' => [
[
'propertyName' => 'hs_object_id',
'operator' => 'IN',
'values' => $dealIds,
],
],
],
],
'limit' => self::MAX_FILTER_SIZE,
];
}
}...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.39660904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.40525267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4162234,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.42486703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.43351063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.44448137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4554521,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.4820479,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.49301863,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.6821808,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.66855055,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.67852396,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6878325,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6974734,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.70478725,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.35904256,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"25","depth":4,"bounds":{"left":0.36901596,"top":0.2490024,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.3806516,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.3879654,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Psr\\Log\\LoggerInterface;\n\nclass PayloadBuilder\n{\n public const int MAX_SEARCH_REQUEST_LIMIT = 200;\n\n private const int MAX_FILTER_SIZE = 100;\n\n private const string SORT_PROPERTY = 'hs_timestamp';\n private const string ENGAGEMENT_MEETINGS = 'meetings';\n\n public function getLinkToTaskPayload(string $objectType, string $objectId, string $engagementType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($engagementType === self::ENGAGEMENT_MEETINGS) {\n $payload['filterGroups'] = $this->buildMeetingFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_timestamp',\n 'hs_activity_type',\n ];\n } else {\n $payload['filterGroups'] = $this->buildTaskFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp'];\n }\n\n return $payload;\n }\n\n public function getRecentlyUpdatedSearchPayload(Carbon $since, ?Carbon $to, array $properties): array\n {\n $lastUpdateDate = in_array('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n public function getByProfileSearchPayload(string $crmId, array $properties, Carbon $since, ?Carbon $to): array\n {\n $lastUpdateDate = array_key_exists('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n [\n 'propertyName' => 'hubspot_owner_id',\n 'operator' => 'EQ',\n 'value' => $crmId,\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n private function buildMeetingFiltersForLinkToTask($objectType, $objectId): array\n {\n $filters = [];\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'IN',\n 'values' => ['SCHEDULED', 'RESCHEDULED'],\n ],\n ],\n ];\n\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ],\n ];\n\n return $filters;\n }\n\n private function buildTaskFiltersForLinkToTask($objectType, $objectId): array\n {\n return [\n [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n ],\n ],\n ];\n }\n\n private function getAssociatedObjectFilter(string $objectType, string $objectId): array\n {\n return [\n 'propertyName' => 'associations.' . $objectType,\n 'operator' => 'EQ',\n 'value' => $objectId,\n ];\n }\n\n public function generatePlaybackURLSearchPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n public function generateGetCallsPayload(Carbon $from, Carbon $to, string $activityProvider, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $activityProvider,\n ],\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function generateSearchCallsByPeriodPayload(Carbon $from, Carbon $to, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $this->getSearchCallAttributes(),\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function getSearchCallAttributes(): array\n {\n return [\n 'hs_timestamp',\n 'hs_call_recording_url',\n 'hs_call_body',\n 'hs_call_status',\n 'hs_call_to_number',\n 'hs_call_from_number',\n 'hs_call_duration',\n 'hs_call_disposition',\n 'hs_call_title',\n 'hs_call_direction',\n 'hubspot_owner_id',\n 'hs_activity_type',\n 'hs_call_external_id',\n 'hs_call_source',\n ];\n }\n\n public function generatePlaybackAddUrlBatchPayload(array $crmUpdateData): array\n {\n $updateObjectsData = [];\n\n foreach ($crmUpdateData as $data) {\n $updateObjectsData[] = [\n 'id' => $data['crm_id'],\n 'properties' => [\n 'hs_call_body' => $data['hs_call_body'] .\n '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">' .\n '<a href=\"' . $data['playback_url'] . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>',\n ],\n ];\n }\n\n return ['inputs' => $updateObjectsData];\n }\n\n public function generateSearchCallByTokenPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload for phone search based on the specified parameters.\n *\n * @param string $phone The phone number to search.\n * @param bool $isAlternativeSearch Indicates if an alternative search should be performed\n * if the first search fails to find a match. The first search request should cover most of the cases.\n */\n public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array\n {\n $filterPropertyNames = $isAlternativeSearch ?\n ['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :\n ['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];\n\n $filterGroups = array_map(function ($propertyName) use ($phone) {\n return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);\n }, $filterPropertyNames);\n\n $properties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n\n return $this->createPayload($filterGroups, $properties);\n }\n\n /**\n * Creates a filter group with the specified parameters.\n */\n private function createFilterGroup(string $propertyName, string $operator, $value): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => $propertyName,\n 'operator' => $operator,\n 'value' => $value,\n ],\n ],\n ];\n }\n\n /**\n * Creates a payload with the specified filter groups and properties.\n */\n private function createPayload(array $filterGroups, array $properties): array\n {\n return [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $properties,\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload to find related activities based on the specified data and object type.\n * The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)\n * and are associated with any of the provided prospect types (contact or company).\n *\n * @param array $data An associative array containing the following keys:\n * - 'from': The start time for the activity search (timestamp).\n * - 'to': The end time for the activity search (timestamp).\n * - 'contact': (optional) The ID of the associated contact.\n * - 'company': (optional) The ID of the associated company.\n * @param string $objectType The type of engagement to search for (meeting or other type like call).\n *\n * @return array The payload array to be used for searching related activities.\n */\n public function getFindRelatedActivityPayload(array $data, string $objectType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($objectType === self::ENGAGEMENT_MEETINGS) {\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_activity_type',\n 'hs_timestamp',\n 'hubspot_owner_id',\n 'hs_meeting_body',\n 'hs_internal_meeting_notes',\n 'hs_meeting_location',\n 'hs_meeting_start_time',\n 'hs_meeting_end_time',\n ];\n } else {\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];\n }\n\n $timeFiltersWithEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'LTE',\n 'value' => $data['to'],\n ],\n ];\n\n $timeFiltersWithoutEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ];\n\n $payload['filterGroups'] = [];\n\n $associationTypes = ['contact', 'company'];\n foreach ($associationTypes as $type) {\n if (! empty($data[$type])) {\n $filterGroupWithEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithEndTime;\n\n $filterGroupWithoutEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithoutEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithoutEndTime ;\n }\n }\n\n return $payload;\n }\n\n /** Parameters\n * [\n * 'accountId' => $crmAccountId,\n * 'sortBy' => $sortBy,\n * 'sortDir' => $sortDir,\n * 'onlyOpen' => $onlyOpen,\n * 'closedStages' => $this->getClosedDealStages(),\n * 'userId' => $userId,\n * ];\n */\n public function generateOpportunitiesSearchPayload(\n Configuration $config,\n string $crmAccountId,\n array $closedStages,\n ): array {\n $closedFilters = [];\n $filterGroups = [];\n $onlyOpen = true;\n\n switch ($config->getOpportunityAssignmentRule()) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'ASCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n default:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n $onlyOpen = false;\n }\n\n $baseFilters = [\n [\n 'propertyName' => 'associations.company',\n 'operator' => 'EQ',\n 'value' => $crmAccountId,\n ],\n ];\n\n // Handle closed stages in chunks\n if ($onlyOpen) {\n foreach (['won', 'lost'] as $key) {\n if (! empty($closedStages[$key])) {\n $chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);\n foreach ($chunks as $chunk) {\n $closedFilters[] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $chunk,\n ];\n }\n }\n }\n }\n\n $filterGroups[] = [\n 'filters' => array_merge($baseFilters, $closedFilters),\n ];\n\n $payload = [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => $sortBy,\n 'direction' => $sortDir,\n ],\n ],\n 'properties' => [\n 'dealname',\n 'amount',\n 'hubspot_owner_id',\n 'pipeline',\n 'dealstage',\n 'closedate',\n 'deal_currency_code',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n $logger = app(LoggerInterface::class);\n $logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * Converts v1 payload data to v3\n */\n public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array\n {\n // Use this mapping until the whole Hubspot V1 is dropped.\n $properties = [\n 'hubspot_owner_id' => $engagement['ownerId'],\n 'hs_timestamp' => $engagement['timestamp'],\n ];\n\n // $engagement['activityType'] is $activity->category->name\n if (isset($engagement['activityType'])) {\n $properties['hs_activity_type'] = $engagement['activityType'];\n }\n\n $metadataKeyMap = [\n 'hs_meeting_outcome' => 'meetingOutcome',\n 'hs_meeting_title' => 'title',\n 'hs_meeting_start_time' => 'startTime',\n 'hs_meeting_end_time' => 'endTime',\n 'hs_meeting_body' => 'body',\n 'hs_internal_meeting_notes' => 'internalMeetingNotes',\n ];\n\n foreach ($metadataKeyMap as $newKey => $oldKey) {\n if (isset($metadata[$oldKey])) {\n $properties[$newKey] = $metadata[$oldKey];\n unset($metadata[$oldKey]);\n }\n }\n\n $properties = [\n ...$properties,\n ...$metadata, // custom fields\n ];\n\n $response = [\n 'properties' => $properties,\n ];\n\n if (! empty($associations)) {\n $response['associations'] = $associations;\n }\n\n return $response;\n }\n\n /**\n * Generate a payload to search for contacts by name.\n * The search is a token search in firstname or lastname\n */\n public function generateSearchContactsByNamePayload(string $name, array $fields): array\n {\n $firstNameFilter = [\n 'propertyName' => 'firstname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n $lastNameFilter = [\n 'propertyName' => 'lastname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n return [\n 'filterGroups' => [\n [\n 'filters' => [$firstNameFilter],\n ],\n [\n 'filters' => [$lastNameFilter],\n ],\n ],\n 'properties' => $fields,\n 'sorts' => [\n [\n 'propertyName' => 'lastmodifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n ];\n }\n\n public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array\n {\n $inputs = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $inputs[] = [\n 'from' => [\n 'id' => $crmId,\n ],\n 'to' => [\n 'id' => (string) $id,\n ],\n 'types' => [\n [\n 'associationCategory' => 'HUBSPOT_DEFINED',\n 'associationTypeId' => $associationType,\n ],\n ],\n ];\n }\n\n return ['inputs' => $inputs];\n }\n\n public function buildRemoveAssociationPayload(string $crmId, array $ids): array\n {\n $toArray = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $toArray[] = ['id' => (string) $id];\n }\n\n return [\n 'inputs' => [\n [\n 'from' => ['id' => $crmId],\n 'to' => $toArray,\n ],\n ],\n ];\n }\n\n public function addClosedStageFilters(array &$payload, array $closedStages): void\n {\n if (! empty($closedStages['won'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['won'],\n ];\n }\n\n if (! empty($closedStages['lost'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['lost'],\n ];\n }\n }\n\n public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void\n {\n $payload['filters'][] = [\n 'propertyName' => 'createdate',\n 'operator' => 'GT',\n 'value' => $createdAfter->getPreciseTimestamp(3),\n ];\n }\n\n public function getDealsInBulkPayload(array $dealIds): array\n {\n return [\n 'filterGroups' => [\n [\n 'filters' => [\n [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'IN',\n 'values' => $dealIds,\n ],\n ],\n ],\n ],\n 'limit' => self::MAX_FILTER_SIZE,\n ];\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.070231445,"width":0.35738033,"height":0.92976856},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Psr\\Log\\LoggerInterface;\n\nclass PayloadBuilder\n{\n public const int MAX_SEARCH_REQUEST_LIMIT = 200;\n\n private const int MAX_FILTER_SIZE = 100;\n\n private const string SORT_PROPERTY = 'hs_timestamp';\n private const string ENGAGEMENT_MEETINGS = 'meetings';\n\n public function getLinkToTaskPayload(string $objectType, string $objectId, string $engagementType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($engagementType === self::ENGAGEMENT_MEETINGS) {\n $payload['filterGroups'] = $this->buildMeetingFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_timestamp',\n 'hs_activity_type',\n ];\n } else {\n $payload['filterGroups'] = $this->buildTaskFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp'];\n }\n\n return $payload;\n }\n\n public function getRecentlyUpdatedSearchPayload(Carbon $since, ?Carbon $to, array $properties): array\n {\n $lastUpdateDate = in_array('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n public function getByProfileSearchPayload(string $crmId, array $properties, Carbon $since, ?Carbon $to): array\n {\n $lastUpdateDate = array_key_exists('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n [\n 'propertyName' => 'hubspot_owner_id',\n 'operator' => 'EQ',\n 'value' => $crmId,\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n private function buildMeetingFiltersForLinkToTask($objectType, $objectId): array\n {\n $filters = [];\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'IN',\n 'values' => ['SCHEDULED', 'RESCHEDULED'],\n ],\n ],\n ];\n\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ],\n ];\n\n return $filters;\n }\n\n private function buildTaskFiltersForLinkToTask($objectType, $objectId): array\n {\n return [\n [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n ],\n ],\n ];\n }\n\n private function getAssociatedObjectFilter(string $objectType, string $objectId): array\n {\n return [\n 'propertyName' => 'associations.' . $objectType,\n 'operator' => 'EQ',\n 'value' => $objectId,\n ];\n }\n\n public function generatePlaybackURLSearchPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n public function generateGetCallsPayload(Carbon $from, Carbon $to, string $activityProvider, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $activityProvider,\n ],\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function generateSearchCallsByPeriodPayload(Carbon $from, Carbon $to, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $this->getSearchCallAttributes(),\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function getSearchCallAttributes(): array\n {\n return [\n 'hs_timestamp',\n 'hs_call_recording_url',\n 'hs_call_body',\n 'hs_call_status',\n 'hs_call_to_number',\n 'hs_call_from_number',\n 'hs_call_duration',\n 'hs_call_disposition',\n 'hs_call_title',\n 'hs_call_direction',\n 'hubspot_owner_id',\n 'hs_activity_type',\n 'hs_call_external_id',\n 'hs_call_source',\n ];\n }\n\n public function generatePlaybackAddUrlBatchPayload(array $crmUpdateData): array\n {\n $updateObjectsData = [];\n\n foreach ($crmUpdateData as $data) {\n $updateObjectsData[] = [\n 'id' => $data['crm_id'],\n 'properties' => [\n 'hs_call_body' => $data['hs_call_body'] .\n '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">' .\n '<a href=\"' . $data['playback_url'] . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>',\n ],\n ];\n }\n\n return ['inputs' => $updateObjectsData];\n }\n\n public function generateSearchCallByTokenPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload for phone search based on the specified parameters.\n *\n * @param string $phone The phone number to search.\n * @param bool $isAlternativeSearch Indicates if an alternative search should be performed\n * if the first search fails to find a match. The first search request should cover most of the cases.\n */\n public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array\n {\n $filterPropertyNames = $isAlternativeSearch ?\n ['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :\n ['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];\n\n $filterGroups = array_map(function ($propertyName) use ($phone) {\n return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);\n }, $filterPropertyNames);\n\n $properties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n\n return $this->createPayload($filterGroups, $properties);\n }\n\n /**\n * Creates a filter group with the specified parameters.\n */\n private function createFilterGroup(string $propertyName, string $operator, $value): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => $propertyName,\n 'operator' => $operator,\n 'value' => $value,\n ],\n ],\n ];\n }\n\n /**\n * Creates a payload with the specified filter groups and properties.\n */\n private function createPayload(array $filterGroups, array $properties): array\n {\n return [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $properties,\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload to find related activities based on the specified data and object type.\n * The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)\n * and are associated with any of the provided prospect types (contact or company).\n *\n * @param array $data An associative array containing the following keys:\n * - 'from': The start time for the activity search (timestamp).\n * - 'to': The end time for the activity search (timestamp).\n * - 'contact': (optional) The ID of the associated contact.\n * - 'company': (optional) The ID of the associated company.\n * @param string $objectType The type of engagement to search for (meeting or other type like call).\n *\n * @return array The payload array to be used for searching related activities.\n */\n public function getFindRelatedActivityPayload(array $data, string $objectType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($objectType === self::ENGAGEMENT_MEETINGS) {\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_activity_type',\n 'hs_timestamp',\n 'hubspot_owner_id',\n 'hs_meeting_body',\n 'hs_internal_meeting_notes',\n 'hs_meeting_location',\n 'hs_meeting_start_time',\n 'hs_meeting_end_time',\n ];\n } else {\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];\n }\n\n $timeFiltersWithEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'LTE',\n 'value' => $data['to'],\n ],\n ];\n\n $timeFiltersWithoutEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ];\n\n $payload['filterGroups'] = [];\n\n $associationTypes = ['contact', 'company'];\n foreach ($associationTypes as $type) {\n if (! empty($data[$type])) {\n $filterGroupWithEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithEndTime;\n\n $filterGroupWithoutEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithoutEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithoutEndTime ;\n }\n }\n\n return $payload;\n }\n\n /** Parameters\n * [\n * 'accountId' => $crmAccountId,\n * 'sortBy' => $sortBy,\n * 'sortDir' => $sortDir,\n * 'onlyOpen' => $onlyOpen,\n * 'closedStages' => $this->getClosedDealStages(),\n * 'userId' => $userId,\n * ];\n */\n public function generateOpportunitiesSearchPayload(\n Configuration $config,\n string $crmAccountId,\n array $closedStages,\n ): array {\n $closedFilters = [];\n $filterGroups = [];\n $onlyOpen = true;\n\n switch ($config->getOpportunityAssignmentRule()) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'ASCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n default:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n $onlyOpen = false;\n }\n\n $baseFilters = [\n [\n 'propertyName' => 'associations.company',\n 'operator' => 'EQ',\n 'value' => $crmAccountId,\n ],\n ];\n\n // Handle closed stages in chunks\n if ($onlyOpen) {\n foreach (['won', 'lost'] as $key) {\n if (! empty($closedStages[$key])) {\n $chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);\n foreach ($chunks as $chunk) {\n $closedFilters[] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $chunk,\n ];\n }\n }\n }\n }\n\n $filterGroups[] = [\n 'filters' => array_merge($baseFilters, $closedFilters),\n ];\n\n $payload = [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => $sortBy,\n 'direction' => $sortDir,\n ],\n ],\n 'properties' => [\n 'dealname',\n 'amount',\n 'hubspot_owner_id',\n 'pipeline',\n 'dealstage',\n 'closedate',\n 'deal_currency_code',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n $logger = app(LoggerInterface::class);\n $logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * Converts v1 payload data to v3\n */\n public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array\n {\n // Use this mapping until the whole Hubspot V1 is dropped.\n $properties = [\n 'hubspot_owner_id' => $engagement['ownerId'],\n 'hs_timestamp' => $engagement['timestamp'],\n ];\n\n // $engagement['activityType'] is $activity->category->name\n if (isset($engagement['activityType'])) {\n $properties['hs_activity_type'] = $engagement['activityType'];\n }\n\n $metadataKeyMap = [\n 'hs_meeting_outcome' => 'meetingOutcome',\n 'hs_meeting_title' => 'title',\n 'hs_meeting_start_time' => 'startTime',\n 'hs_meeting_end_time' => 'endTime',\n 'hs_meeting_body' => 'body',\n 'hs_internal_meeting_notes' => 'internalMeetingNotes',\n ];\n\n foreach ($metadataKeyMap as $newKey => $oldKey) {\n if (isset($metadata[$oldKey])) {\n $properties[$newKey] = $metadata[$oldKey];\n unset($metadata[$oldKey]);\n }\n }\n\n $properties = [\n ...$properties,\n ...$metadata, // custom fields\n ];\n\n $response = [\n 'properties' => $properties,\n ];\n\n if (! empty($associations)) {\n $response['associations'] = $associations;\n }\n\n return $response;\n }\n\n /**\n * Generate a payload to search for contacts by name.\n * The search is a token search in firstname or lastname\n */\n public function generateSearchContactsByNamePayload(string $name, array $fields): array\n {\n $firstNameFilter = [\n 'propertyName' => 'firstname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n $lastNameFilter = [\n 'propertyName' => 'lastname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n return [\n 'filterGroups' => [\n [\n 'filters' => [$firstNameFilter],\n ],\n [\n 'filters' => [$lastNameFilter],\n ],\n ],\n 'properties' => $fields,\n 'sorts' => [\n [\n 'propertyName' => 'lastmodifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n ];\n }\n\n public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array\n {\n $inputs = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $inputs[] = [\n 'from' => [\n 'id' => $crmId,\n ],\n 'to' => [\n 'id' => (string) $id,\n ],\n 'types' => [\n [\n 'associationCategory' => 'HUBSPOT_DEFINED',\n 'associationTypeId' => $associationType,\n ],\n ],\n ];\n }\n\n return ['inputs' => $inputs];\n }\n\n public function buildRemoveAssociationPayload(string $crmId, array $ids): array\n {\n $toArray = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $toArray[] = ['id' => (string) $id];\n }\n\n return [\n 'inputs' => [\n [\n 'from' => ['id' => $crmId],\n 'to' => $toArray,\n ],\n ],\n ];\n }\n\n public function addClosedStageFilters(array &$payload, array $closedStages): void\n {\n if (! empty($closedStages['won'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['won'],\n ];\n }\n\n if (! empty($closedStages['lost'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['lost'],\n ];\n }\n }\n\n public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void\n {\n $payload['filters'][] = [\n 'propertyName' => 'createdate',\n 'operator' => 'GT',\n 'value' => $createdAfter->getPreciseTimestamp(3),\n ];\n }\n\n public function getDealsInBulkPayload(array $dealIds): array\n {\n return [\n 'filterGroups' => [\n [\n 'filters' => [\n [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'IN',\n 'values' => $dealIds,\n ],\n ],\n ],\n ],\n 'limit' => self::MAX_FILTER_SIZE,\n ];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
-1326413346595229327
|
2074988155737684557
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] string $phone The phone number to search.
* @param bool $isAlternativeSearch Indicates if an alternative search should be performed
* if the first search fails to find a match. The first search request should cover most of the cases.
*/
public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array
{
$filterPropertyNames = $isAlternativeSearch ?
['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :
['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];
$filterGroups = array_map(function ($propertyName) use ($phone) {
return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);
}, $filterPropertyNames);
$properties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
return $this->createPayload($filterGroups, $properties);
}
/**
* Creates a filter group with the specified parameters.
*/
private function createFilterGroup(string $propertyName, string $operator, $value): array
{
return [
'filters' => [
[
'propertyName' => $propertyName,
'operator' => $operator,
'value' => $value,
],
],
];
}
/**
* Creates a payload with the specified filter groups and properties.
*/
private function createPayload(array $filterGroups, array $properties): array
{
return [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => 'modifieddate',
'direction' => 'DESCENDING',
],
],
'properties' => $properties,
'limit' => 1,
];
}
/**
* Generates a payload to find related activities based on the specified data and object type.
* The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)
* and are associated with any of the provided prospect types (contact or company).
*
* @param array $data An associative array containing the following keys:
* - 'from': The start time for the activity search (timestamp).
* - 'to': The end time for the activity search (timestamp).
* - 'contact': (optional) The ID of the associated contact.
* - 'company': (optional) The ID of the associated company.
* @param string $objectType The type of engagement to search for (meeting or other type like call).
*
* @return array The payload array to be used for searching related activities.
*/
public function getFindRelatedActivityPayload(array $data, string $objectType): array
{
$payload = [
'sorts' => [
[
'propertyName' => self::SORT_PROPERTY,
'direction' => 'DESCENDING',
],
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
if ($objectType === self::ENGAGEMENT_MEETINGS) {
$payload['properties'] = [
'hs_meeting_title',
'hs_meeting_outcome',
'hs_activity_type',
'hs_timestamp',
'hubspot_owner_id',
'hs_meeting_body',
'hs_internal_meeting_notes',
'hs_meeting_location',
'hs_meeting_start_time',
'hs_meeting_end_time',
];
} else {
$payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];
}
$timeFiltersWithEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'LTE',
'value' => $data['to'],
],
];
$timeFiltersWithoutEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'NOT_HAS_PROPERTY',
],
];
$payload['filterGroups'] = [];
$associationTypes = ['contact', 'company'];
foreach ($associationTypes as $type) {
if (! empty($data[$type])) {
$filterGroupWithEndTime = [
'filters' => array_merge(
$timeFiltersWithEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithEndTime;
$filterGroupWithoutEndTime = [
'filters' => array_merge(
$timeFiltersWithoutEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithoutEndTime ;
}
}
return $payload;
}
/** Parameters
* [
* 'accountId' => $crmAccountId,
* 'sortBy' => $sortBy,
* 'sortDir' => $sortDir,
* 'onlyOpen' => $onlyOpen,
* 'closedStages' => $this->getClosedDealStages(),
* 'userId' => $userId,
* ];
*/
public function generateOpportunitiesSearchPayload(
Configuration $config,
string $crmAccountId,
array $closedStages,
): array {
$closedFilters = [];
$filterGroups = [];
$onlyOpen = true;
switch ($config->getOpportunityAssignmentRule()) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$sortBy = 'createdate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$sortBy = 'createdate';
$sortDir = 'ASCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
default:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
$onlyOpen = false;
}
$baseFilters = [
[
'propertyName' => 'associations.company',
'operator' => 'EQ',
'value' => $crmAccountId,
],
];
// Handle closed stages in chunks
if ($onlyOpen) {
foreach (['won', 'lost'] as $key) {
if (! empty($closedStages[$key])) {
$chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);
foreach ($chunks as $chunk) {
$closedFilters[] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $chunk,
];
}
}
}
}
$filterGroups[] = [
'filters' => array_merge($baseFilters, $closedFilters),
];
$payload = [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => $sortBy,
'direction' => $sortDir,
],
],
'properties' => [
'dealname',
'amount',
'hubspot_owner_id',
'pipeline',
'dealstage',
'closedate',
'deal_currency_code',
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
$logger = app(LoggerInterface::class);
$logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
return $payload;
}
/**
* Converts v1 payload data to v3
*/
public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array
{
// Use this mapping until the whole Hubspot V1 is dropped.
$properties = [
'hubspot_owner_id' => $engagement['ownerId'],
'hs_timestamp' => $engagement['timestamp'],
];
// $engagement['activityType'] is $activity->category->name
if (isset($engagement['activityType'])) {
$properties['hs_activity_type'] = $engagement['activityType'];
}
$metadataKeyMap = [
'hs_meeting_outcome' => 'meetingOutcome',
'hs_meeting_title' => 'title',
'hs_meeting_start_time' => 'startTime',
'hs_meeting_end_time' => 'endTime',
'hs_meeting_body' => 'body',
'hs_internal_meeting_notes' => 'internalMeetingNotes',
];
foreach ($metadataKeyMap as $newKey => $oldKey) {
if (isset($metadata[$oldKey])) {
$properties[$newKey] = $metadata[$oldKey];
unset($metadata[$oldKey]);
}
}
$properties = [
...$properties,
...$metadata, // custom fields
];
$response = [
'properties' => $properties,
];
if (! empty($associations)) {
$response['associations'] = $associations;
}
return $response;
}
/**
* Generate a payload to search for contacts by name.
* The search is a token search in firstname or lastname
*/
public function generateSearchContactsByNamePayload(string $name, array $fields): array
{
$firstNameFilter = [
'propertyName' => 'firstname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
$lastNameFilter = [
'propertyName' => 'lastname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
return [
'filterGroups' => [
[
'filters' => [$firstNameFilter],
],
[
'filters' => [$lastNameFilter],
],
],
'properties' => $fields,
'sorts' => [
[
'propertyName' => 'lastmodifieddate',
'direction' => 'DESCENDING',
],
],
];
}
public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array
{
$inputs = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$inputs[] = [
'from' => [
'id' => $crmId,
],
'to' => [
'id' => (string) $id,
],
'types' => [
[
'associationCategory' => 'HUBSPOT_DEFINED',
'associationTypeId' => $associationType,
],
],
];
}
return ['inputs' => $inputs];
}
public function buildRemoveAssociationPayload(string $crmId, array $ids): array
{
$toArray = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$toArray[] = ['id' => (string) $id];
}
return [
'inputs' => [
[
'from' => ['id' => $crmId],
'to' => $toArray,
],
],
];
}
public function addClosedStageFilters(array &$payload, array $closedStages): void
{
if (! empty($closedStages['won'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['won'],
];
}
if (! empty($closedStages['lost'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['lost'],
];
}
}
public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void
{
$payload['filters'][] = [
'propertyName' => 'createdate',
'operator' => 'GT',
'value' => $createdAfter->getPreciseTimestamp(3),
];
}
public function getDealsInBulkPayload(array $dealIds): array
{
return [
'filterGroups' => [
[
'filters' => [
[
'propertyName' => 'hs_object_id',
'operator' => 'IN',
'values' => $dealIds,
],
],
],
],
'limit' => self::MAX_FILTER_SIZE,
];
}
}...
|
4831
|
NULL
|
NULL
|
NULL
|
|
4833
|
174
|
28
|
2026-05-07T14:31:27.031494+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164287031_m2.jpg...
|
PhpStorm
|
faVsco.js – PayloadBuilder.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] string $phone The phone number to search.
* @param bool $isAlternativeSearch Indicates if an alternative search should be performed
* if the first search fails to find a match. The first search request should cover most of the cases.
*/
public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array
{
$filterPropertyNames = $isAlternativeSearch ?
['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :
['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];
$filterGroups = array_map(function ($propertyName) use ($phone) {
return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);
}, $filterPropertyNames);
$properties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
return $this->createPayload($filterGroups, $properties);
}
/**
* Creates a filter group with the specified parameters.
*/
private function createFilterGroup(string $propertyName, string $operator, $value): array
{
return [
'filters' => [
[
'propertyName' => $propertyName,
'operator' => $operator,
'value' => $value,
],
],
];
}
/**
* Creates a payload with the specified filter groups and properties.
*/
private function createPayload(array $filterGroups, array $properties): array
{
return [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => 'modifieddate',
'direction' => 'DESCENDING',
],
],
'properties' => $properties,
'limit' => 1,
];
}
/**
* Generates a payload to find related activities based on the specified data and object type.
* The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)
* and are associated with any of the provided prospect types (contact or company).
*
* @param array $data An associative array containing the following keys:
* - 'from': The start time for the activity search (timestamp).
* - 'to': The end time for the activity search (timestamp).
* - 'contact': (optional) The ID of the associated contact.
* - 'company': (optional) The ID of the associated company.
* @param string $objectType The type of engagement to search for (meeting or other type like call).
*
* @return array The payload array to be used for searching related activities.
*/
public function getFindRelatedActivityPayload(array $data, string $objectType): array
{
$payload = [
'sorts' => [
[
'propertyName' => self::SORT_PROPERTY,
'direction' => 'DESCENDING',
],
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
if ($objectType === self::ENGAGEMENT_MEETINGS) {
$payload['properties'] = [
'hs_meeting_title',
'hs_meeting_outcome',
'hs_activity_type',
'hs_timestamp',
'hubspot_owner_id',
'hs_meeting_body',
'hs_internal_meeting_notes',
'hs_meeting_location',
'hs_meeting_start_time',
'hs_meeting_end_time',
];
} else {
$payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];
}
$timeFiltersWithEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'LTE',
'value' => $data['to'],
],
];
$timeFiltersWithoutEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'NOT_HAS_PROPERTY',
],
];
$payload['filterGroups'] = [];
$associationTypes = ['contact', 'company'];
foreach ($associationTypes as $type) {
if (! empty($data[$type])) {
$filterGroupWithEndTime = [
'filters' => array_merge(
$timeFiltersWithEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithEndTime;
$filterGroupWithoutEndTime = [
'filters' => array_merge(
$timeFiltersWithoutEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithoutEndTime ;
}
}
return $payload;
}
/** Parameters
* [
* 'accountId' => $crmAccountId,
* 'sortBy' => $sortBy,
* 'sortDir' => $sortDir,
* 'onlyOpen' => $onlyOpen,
* 'closedStages' => $this->getClosedDealStages(),
* 'userId' => $userId,
* ];
*/
public function generateOpportunitiesSearchPayload(
Configuration $config,
string $crmAccountId,
array $closedStages,
): array {
$closedFilters = [];
$filterGroups = [];
$onlyOpen = true;
switch ($config->getOpportunityAssignmentRule()) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$sortBy = 'createdate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$sortBy = 'createdate';
$sortDir = 'ASCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
default:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
$onlyOpen = false;
}
$baseFilters = [
[
'propertyName' => 'associations.company',
'operator' => 'EQ',
'value' => $crmAccountId,
],
];
// Handle closed stages in chunks
if ($onlyOpen) {
foreach (['won', 'lost'] as $key) {
if (! empty($closedStages[$key])) {
$chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);
foreach ($chunks as $chunk) {
$closedFilters[] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $chunk,
];
}
}
}
}
$filterGroups[] = [
'filters' => array_merge($baseFilters, $closedFilters),
];
$payload = [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => $sortBy,
'direction' => $sortDir,
],
],
'properties' => [
'dealname',
'amount',
'hubspot_owner_id',
'pipeline',
'dealstage',
'closedate',
'deal_currency_code',
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
$logger = app(LoggerInterface::class);
$logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
return $payload;
}
/**
* Converts v1 payload data to v3
*/
public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array
{
// Use this mapping until the whole Hubspot V1 is dropped.
$properties = [
'hubspot_owner_id' => $engagement['ownerId'],
'hs_timestamp' => $engagement['timestamp'],
];
// $engagement['activityType'] is $activity->category->name
if (isset($engagement['activityType'])) {
$properties['hs_activity_type'] = $engagement['activityType'];
}
$metadataKeyMap = [
'hs_meeting_outcome' => 'meetingOutcome',
'hs_meeting_title' => 'title',
'hs_meeting_start_time' => 'startTime',
'hs_meeting_end_time' => 'endTime',
'hs_meeting_body' => 'body',
'hs_internal_meeting_notes' => 'internalMeetingNotes',
];
foreach ($metadataKeyMap as $newKey => $oldKey) {
if (isset($metadata[$oldKey])) {
$properties[$newKey] = $metadata[$oldKey];
unset($metadata[$oldKey]);
}
}
$properties = [
...$properties,
...$metadata, // custom fields
];
$response = [
'properties' => $properties,
];
if (! empty($associations)) {
$response['associations'] = $associations;
}
return $response;
}
/**
* Generate a payload to search for contacts by name.
* The search is a token search in firstname or lastname
*/
public function generateSearchContactsByNamePayload(string $name, array $fields): array
{
$firstNameFilter = [
'propertyName' => 'firstname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
$lastNameFilter = [
'propertyName' => 'lastname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
return [
'filterGroups' => [
[
'filters' => [$firstNameFilter],
],
[
'filters' => [$lastNameFilter],
],
],
'properties' => $fields,
'sorts' => [
[
'propertyName' => 'lastmodifieddate',
'direction' => 'DESCENDING',
],
],
];
}
public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array
{
$inputs = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$inputs[] = [
'from' => [
'id' => $crmId,
],
'to' => [
'id' => (string) $id,
],
'types' => [
[
'associationCategory' => 'HUBSPOT_DEFINED',
'associationTypeId' => $associationType,
],
],
];
}
return ['inputs' => $inputs];
}
public function buildRemoveAssociationPayload(string $crmId, array $ids): array
{
$toArray = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$toArray[] = ['id' => (string) $id];
}
return [
'inputs' => [
[
'from' => ['id' => $crmId],
'to' => $toArray,
],
],
];
}
public function addClosedStageFilters(array &$payload, array $closedStages): void
{
if (! empty($closedStages['won'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['won'],
];
}
if (! empty($closedStages['lost'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['lost'],
];
}
}
public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void
{
$payload['filters'][] = [
'propertyName' => 'createdate',
'operator' => 'GT',
'value' => $createdAfter->getPreciseTimestamp(3),
];
}
public function getDealsInBulkPayload(array $dealIds): array
{
return [
'filterGroups' => [
[
'filters' => [
[
'propertyName' => 'hs_object_id',
'operator' => 'IN',
'values' => $dealIds,
],
],
],
],
'limit' => self::MAX_FILTER_SIZE,
];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.39660904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.40525267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4162234,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.42486703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.43351063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.44448137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4554521,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.4820479,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.49301863,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.6821808,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.66855055,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.67852396,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6878325,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6974734,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.70478725,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.35904256,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"25","depth":4,"bounds":{"left":0.36901596,"top":0.2490024,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.3806516,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.3879654,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Psr\\Log\\LoggerInterface;\n\nclass PayloadBuilder\n{\n public const int MAX_SEARCH_REQUEST_LIMIT = 200;\n\n private const int MAX_FILTER_SIZE = 100;\n\n private const string SORT_PROPERTY = 'hs_timestamp';\n private const string ENGAGEMENT_MEETINGS = 'meetings';\n\n public function getLinkToTaskPayload(string $objectType, string $objectId, string $engagementType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($engagementType === self::ENGAGEMENT_MEETINGS) {\n $payload['filterGroups'] = $this->buildMeetingFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_timestamp',\n 'hs_activity_type',\n ];\n } else {\n $payload['filterGroups'] = $this->buildTaskFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp'];\n }\n\n return $payload;\n }\n\n public function getRecentlyUpdatedSearchPayload(Carbon $since, ?Carbon $to, array $properties): array\n {\n $lastUpdateDate = in_array('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n public function getByProfileSearchPayload(string $crmId, array $properties, Carbon $since, ?Carbon $to): array\n {\n $lastUpdateDate = array_key_exists('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n [\n 'propertyName' => 'hubspot_owner_id',\n 'operator' => 'EQ',\n 'value' => $crmId,\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n private function buildMeetingFiltersForLinkToTask($objectType, $objectId): array\n {\n $filters = [];\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'IN',\n 'values' => ['SCHEDULED', 'RESCHEDULED'],\n ],\n ],\n ];\n\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ],\n ];\n\n return $filters;\n }\n\n private function buildTaskFiltersForLinkToTask($objectType, $objectId): array\n {\n return [\n [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n ],\n ],\n ];\n }\n\n private function getAssociatedObjectFilter(string $objectType, string $objectId): array\n {\n return [\n 'propertyName' => 'associations.' . $objectType,\n 'operator' => 'EQ',\n 'value' => $objectId,\n ];\n }\n\n public function generatePlaybackURLSearchPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n public function generateGetCallsPayload(Carbon $from, Carbon $to, string $activityProvider, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $activityProvider,\n ],\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function generateSearchCallsByPeriodPayload(Carbon $from, Carbon $to, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $this->getSearchCallAttributes(),\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function getSearchCallAttributes(): array\n {\n return [\n 'hs_timestamp',\n 'hs_call_recording_url',\n 'hs_call_body',\n 'hs_call_status',\n 'hs_call_to_number',\n 'hs_call_from_number',\n 'hs_call_duration',\n 'hs_call_disposition',\n 'hs_call_title',\n 'hs_call_direction',\n 'hubspot_owner_id',\n 'hs_activity_type',\n 'hs_call_external_id',\n 'hs_call_source',\n ];\n }\n\n public function generatePlaybackAddUrlBatchPayload(array $crmUpdateData): array\n {\n $updateObjectsData = [];\n\n foreach ($crmUpdateData as $data) {\n $updateObjectsData[] = [\n 'id' => $data['crm_id'],\n 'properties' => [\n 'hs_call_body' => $data['hs_call_body'] .\n '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">' .\n '<a href=\"' . $data['playback_url'] . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>',\n ],\n ];\n }\n\n return ['inputs' => $updateObjectsData];\n }\n\n public function generateSearchCallByTokenPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload for phone search based on the specified parameters.\n *\n * @param string $phone The phone number to search.\n * @param bool $isAlternativeSearch Indicates if an alternative search should be performed\n * if the first search fails to find a match. The first search request should cover most of the cases.\n */\n public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array\n {\n $filterPropertyNames = $isAlternativeSearch ?\n ['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :\n ['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];\n\n $filterGroups = array_map(function ($propertyName) use ($phone) {\n return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);\n }, $filterPropertyNames);\n\n $properties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n\n return $this->createPayload($filterGroups, $properties);\n }\n\n /**\n * Creates a filter group with the specified parameters.\n */\n private function createFilterGroup(string $propertyName, string $operator, $value): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => $propertyName,\n 'operator' => $operator,\n 'value' => $value,\n ],\n ],\n ];\n }\n\n /**\n * Creates a payload with the specified filter groups and properties.\n */\n private function createPayload(array $filterGroups, array $properties): array\n {\n return [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $properties,\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload to find related activities based on the specified data and object type.\n * The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)\n * and are associated with any of the provided prospect types (contact or company).\n *\n * @param array $data An associative array containing the following keys:\n * - 'from': The start time for the activity search (timestamp).\n * - 'to': The end time for the activity search (timestamp).\n * - 'contact': (optional) The ID of the associated contact.\n * - 'company': (optional) The ID of the associated company.\n * @param string $objectType The type of engagement to search for (meeting or other type like call).\n *\n * @return array The payload array to be used for searching related activities.\n */\n public function getFindRelatedActivityPayload(array $data, string $objectType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($objectType === self::ENGAGEMENT_MEETINGS) {\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_activity_type',\n 'hs_timestamp',\n 'hubspot_owner_id',\n 'hs_meeting_body',\n 'hs_internal_meeting_notes',\n 'hs_meeting_location',\n 'hs_meeting_start_time',\n 'hs_meeting_end_time',\n ];\n } else {\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];\n }\n\n $timeFiltersWithEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'LTE',\n 'value' => $data['to'],\n ],\n ];\n\n $timeFiltersWithoutEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ];\n\n $payload['filterGroups'] = [];\n\n $associationTypes = ['contact', 'company'];\n foreach ($associationTypes as $type) {\n if (! empty($data[$type])) {\n $filterGroupWithEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithEndTime;\n\n $filterGroupWithoutEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithoutEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithoutEndTime ;\n }\n }\n\n return $payload;\n }\n\n /** Parameters\n * [\n * 'accountId' => $crmAccountId,\n * 'sortBy' => $sortBy,\n * 'sortDir' => $sortDir,\n * 'onlyOpen' => $onlyOpen,\n * 'closedStages' => $this->getClosedDealStages(),\n * 'userId' => $userId,\n * ];\n */\n public function generateOpportunitiesSearchPayload(\n Configuration $config,\n string $crmAccountId,\n array $closedStages,\n ): array {\n $closedFilters = [];\n $filterGroups = [];\n $onlyOpen = true;\n\n switch ($config->getOpportunityAssignmentRule()) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'ASCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n default:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n $onlyOpen = false;\n }\n\n $baseFilters = [\n [\n 'propertyName' => 'associations.company',\n 'operator' => 'EQ',\n 'value' => $crmAccountId,\n ],\n ];\n\n // Handle closed stages in chunks\n if ($onlyOpen) {\n foreach (['won', 'lost'] as $key) {\n if (! empty($closedStages[$key])) {\n $chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);\n foreach ($chunks as $chunk) {\n $closedFilters[] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $chunk,\n ];\n }\n }\n }\n }\n\n $filterGroups[] = [\n 'filters' => array_merge($baseFilters, $closedFilters),\n ];\n\n $payload = [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => $sortBy,\n 'direction' => $sortDir,\n ],\n ],\n 'properties' => [\n 'dealname',\n 'amount',\n 'hubspot_owner_id',\n 'pipeline',\n 'dealstage',\n 'closedate',\n 'deal_currency_code',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n $logger = app(LoggerInterface::class);\n $logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * Converts v1 payload data to v3\n */\n public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array\n {\n // Use this mapping until the whole Hubspot V1 is dropped.\n $properties = [\n 'hubspot_owner_id' => $engagement['ownerId'],\n 'hs_timestamp' => $engagement['timestamp'],\n ];\n\n // $engagement['activityType'] is $activity->category->name\n if (isset($engagement['activityType'])) {\n $properties['hs_activity_type'] = $engagement['activityType'];\n }\n\n $metadataKeyMap = [\n 'hs_meeting_outcome' => 'meetingOutcome',\n 'hs_meeting_title' => 'title',\n 'hs_meeting_start_time' => 'startTime',\n 'hs_meeting_end_time' => 'endTime',\n 'hs_meeting_body' => 'body',\n 'hs_internal_meeting_notes' => 'internalMeetingNotes',\n ];\n\n foreach ($metadataKeyMap as $newKey => $oldKey) {\n if (isset($metadata[$oldKey])) {\n $properties[$newKey] = $metadata[$oldKey];\n unset($metadata[$oldKey]);\n }\n }\n\n $properties = [\n ...$properties,\n ...$metadata, // custom fields\n ];\n\n $response = [\n 'properties' => $properties,\n ];\n\n if (! empty($associations)) {\n $response['associations'] = $associations;\n }\n\n return $response;\n }\n\n /**\n * Generate a payload to search for contacts by name.\n * The search is a token search in firstname or lastname\n */\n public function generateSearchContactsByNamePayload(string $name, array $fields): array\n {\n $firstNameFilter = [\n 'propertyName' => 'firstname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n $lastNameFilter = [\n 'propertyName' => 'lastname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n return [\n 'filterGroups' => [\n [\n 'filters' => [$firstNameFilter],\n ],\n [\n 'filters' => [$lastNameFilter],\n ],\n ],\n 'properties' => $fields,\n 'sorts' => [\n [\n 'propertyName' => 'lastmodifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n ];\n }\n\n public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array\n {\n $inputs = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $inputs[] = [\n 'from' => [\n 'id' => $crmId,\n ],\n 'to' => [\n 'id' => (string) $id,\n ],\n 'types' => [\n [\n 'associationCategory' => 'HUBSPOT_DEFINED',\n 'associationTypeId' => $associationType,\n ],\n ],\n ];\n }\n\n return ['inputs' => $inputs];\n }\n\n public function buildRemoveAssociationPayload(string $crmId, array $ids): array\n {\n $toArray = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $toArray[] = ['id' => (string) $id];\n }\n\n return [\n 'inputs' => [\n [\n 'from' => ['id' => $crmId],\n 'to' => $toArray,\n ],\n ],\n ];\n }\n\n public function addClosedStageFilters(array &$payload, array $closedStages): void\n {\n if (! empty($closedStages['won'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['won'],\n ];\n }\n\n if (! empty($closedStages['lost'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['lost'],\n ];\n }\n }\n\n public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void\n {\n $payload['filters'][] = [\n 'propertyName' => 'createdate',\n 'operator' => 'GT',\n 'value' => $createdAfter->getPreciseTimestamp(3),\n ];\n }\n\n public function getDealsInBulkPayload(array $dealIds): array\n {\n return [\n 'filterGroups' => [\n [\n 'filters' => [\n [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'IN',\n 'values' => $dealIds,\n ],\n ],\n ],\n ],\n 'limit' => self::MAX_FILTER_SIZE,\n ];\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.070231445,"width":0.35738033,"height":0.92976856},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Psr\\Log\\LoggerInterface;\n\nclass PayloadBuilder\n{\n public const int MAX_SEARCH_REQUEST_LIMIT = 200;\n\n private const int MAX_FILTER_SIZE = 100;\n\n private const string SORT_PROPERTY = 'hs_timestamp';\n private const string ENGAGEMENT_MEETINGS = 'meetings';\n\n public function getLinkToTaskPayload(string $objectType, string $objectId, string $engagementType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($engagementType === self::ENGAGEMENT_MEETINGS) {\n $payload['filterGroups'] = $this->buildMeetingFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_timestamp',\n 'hs_activity_type',\n ];\n } else {\n $payload['filterGroups'] = $this->buildTaskFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp'];\n }\n\n return $payload;\n }\n\n public function getRecentlyUpdatedSearchPayload(Carbon $since, ?Carbon $to, array $properties): array\n {\n $lastUpdateDate = in_array('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n public function getByProfileSearchPayload(string $crmId, array $properties, Carbon $since, ?Carbon $to): array\n {\n $lastUpdateDate = array_key_exists('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n [\n 'propertyName' => 'hubspot_owner_id',\n 'operator' => 'EQ',\n 'value' => $crmId,\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n private function buildMeetingFiltersForLinkToTask($objectType, $objectId): array\n {\n $filters = [];\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'IN',\n 'values' => ['SCHEDULED', 'RESCHEDULED'],\n ],\n ],\n ];\n\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ],\n ];\n\n return $filters;\n }\n\n private function buildTaskFiltersForLinkToTask($objectType, $objectId): array\n {\n return [\n [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n ],\n ],\n ];\n }\n\n private function getAssociatedObjectFilter(string $objectType, string $objectId): array\n {\n return [\n 'propertyName' => 'associations.' . $objectType,\n 'operator' => 'EQ',\n 'value' => $objectId,\n ];\n }\n\n public function generatePlaybackURLSearchPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n public function generateGetCallsPayload(Carbon $from, Carbon $to, string $activityProvider, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $activityProvider,\n ],\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function generateSearchCallsByPeriodPayload(Carbon $from, Carbon $to, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $this->getSearchCallAttributes(),\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function getSearchCallAttributes(): array\n {\n return [\n 'hs_timestamp',\n 'hs_call_recording_url',\n 'hs_call_body',\n 'hs_call_status',\n 'hs_call_to_number',\n 'hs_call_from_number',\n 'hs_call_duration',\n 'hs_call_disposition',\n 'hs_call_title',\n 'hs_call_direction',\n 'hubspot_owner_id',\n 'hs_activity_type',\n 'hs_call_external_id',\n 'hs_call_source',\n ];\n }\n\n public function generatePlaybackAddUrlBatchPayload(array $crmUpdateData): array\n {\n $updateObjectsData = [];\n\n foreach ($crmUpdateData as $data) {\n $updateObjectsData[] = [\n 'id' => $data['crm_id'],\n 'properties' => [\n 'hs_call_body' => $data['hs_call_body'] .\n '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">' .\n '<a href=\"' . $data['playback_url'] . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>',\n ],\n ];\n }\n\n return ['inputs' => $updateObjectsData];\n }\n\n public function generateSearchCallByTokenPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload for phone search based on the specified parameters.\n *\n * @param string $phone The phone number to search.\n * @param bool $isAlternativeSearch Indicates if an alternative search should be performed\n * if the first search fails to find a match. The first search request should cover most of the cases.\n */\n public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array\n {\n $filterPropertyNames = $isAlternativeSearch ?\n ['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :\n ['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];\n\n $filterGroups = array_map(function ($propertyName) use ($phone) {\n return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);\n }, $filterPropertyNames);\n\n $properties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n\n return $this->createPayload($filterGroups, $properties);\n }\n\n /**\n * Creates a filter group with the specified parameters.\n */\n private function createFilterGroup(string $propertyName, string $operator, $value): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => $propertyName,\n 'operator' => $operator,\n 'value' => $value,\n ],\n ],\n ];\n }\n\n /**\n * Creates a payload with the specified filter groups and properties.\n */\n private function createPayload(array $filterGroups, array $properties): array\n {\n return [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $properties,\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload to find related activities based on the specified data and object type.\n * The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)\n * and are associated with any of the provided prospect types (contact or company).\n *\n * @param array $data An associative array containing the following keys:\n * - 'from': The start time for the activity search (timestamp).\n * - 'to': The end time for the activity search (timestamp).\n * - 'contact': (optional) The ID of the associated contact.\n * - 'company': (optional) The ID of the associated company.\n * @param string $objectType The type of engagement to search for (meeting or other type like call).\n *\n * @return array The payload array to be used for searching related activities.\n */\n public function getFindRelatedActivityPayload(array $data, string $objectType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($objectType === self::ENGAGEMENT_MEETINGS) {\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_activity_type',\n 'hs_timestamp',\n 'hubspot_owner_id',\n 'hs_meeting_body',\n 'hs_internal_meeting_notes',\n 'hs_meeting_location',\n 'hs_meeting_start_time',\n 'hs_meeting_end_time',\n ];\n } else {\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];\n }\n\n $timeFiltersWithEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'LTE',\n 'value' => $data['to'],\n ],\n ];\n\n $timeFiltersWithoutEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ];\n\n $payload['filterGroups'] = [];\n\n $associationTypes = ['contact', 'company'];\n foreach ($associationTypes as $type) {\n if (! empty($data[$type])) {\n $filterGroupWithEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithEndTime;\n\n $filterGroupWithoutEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithoutEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithoutEndTime ;\n }\n }\n\n return $payload;\n }\n\n /** Parameters\n * [\n * 'accountId' => $crmAccountId,\n * 'sortBy' => $sortBy,\n * 'sortDir' => $sortDir,\n * 'onlyOpen' => $onlyOpen,\n * 'closedStages' => $this->getClosedDealStages(),\n * 'userId' => $userId,\n * ];\n */\n public function generateOpportunitiesSearchPayload(\n Configuration $config,\n string $crmAccountId,\n array $closedStages,\n ): array {\n $closedFilters = [];\n $filterGroups = [];\n $onlyOpen = true;\n\n switch ($config->getOpportunityAssignmentRule()) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'ASCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n default:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n $onlyOpen = false;\n }\n\n $baseFilters = [\n [\n 'propertyName' => 'associations.company',\n 'operator' => 'EQ',\n 'value' => $crmAccountId,\n ],\n ];\n\n // Handle closed stages in chunks\n if ($onlyOpen) {\n foreach (['won', 'lost'] as $key) {\n if (! empty($closedStages[$key])) {\n $chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);\n foreach ($chunks as $chunk) {\n $closedFilters[] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $chunk,\n ];\n }\n }\n }\n }\n\n $filterGroups[] = [\n 'filters' => array_merge($baseFilters, $closedFilters),\n ];\n\n $payload = [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => $sortBy,\n 'direction' => $sortDir,\n ],\n ],\n 'properties' => [\n 'dealname',\n 'amount',\n 'hubspot_owner_id',\n 'pipeline',\n 'dealstage',\n 'closedate',\n 'deal_currency_code',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n $logger = app(LoggerInterface::class);\n $logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * Converts v1 payload data to v3\n */\n public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array\n {\n // Use this mapping until the whole Hubspot V1 is dropped.\n $properties = [\n 'hubspot_owner_id' => $engagement['ownerId'],\n 'hs_timestamp' => $engagement['timestamp'],\n ];\n\n // $engagement['activityType'] is $activity->category->name\n if (isset($engagement['activityType'])) {\n $properties['hs_activity_type'] = $engagement['activityType'];\n }\n\n $metadataKeyMap = [\n 'hs_meeting_outcome' => 'meetingOutcome',\n 'hs_meeting_title' => 'title',\n 'hs_meeting_start_time' => 'startTime',\n 'hs_meeting_end_time' => 'endTime',\n 'hs_meeting_body' => 'body',\n 'hs_internal_meeting_notes' => 'internalMeetingNotes',\n ];\n\n foreach ($metadataKeyMap as $newKey => $oldKey) {\n if (isset($metadata[$oldKey])) {\n $properties[$newKey] = $metadata[$oldKey];\n unset($metadata[$oldKey]);\n }\n }\n\n $properties = [\n ...$properties,\n ...$metadata, // custom fields\n ];\n\n $response = [\n 'properties' => $properties,\n ];\n\n if (! empty($associations)) {\n $response['associations'] = $associations;\n }\n\n return $response;\n }\n\n /**\n * Generate a payload to search for contacts by name.\n * The search is a token search in firstname or lastname\n */\n public function generateSearchContactsByNamePayload(string $name, array $fields): array\n {\n $firstNameFilter = [\n 'propertyName' => 'firstname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n $lastNameFilter = [\n 'propertyName' => 'lastname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n return [\n 'filterGroups' => [\n [\n 'filters' => [$firstNameFilter],\n ],\n [\n 'filters' => [$lastNameFilter],\n ],\n ],\n 'properties' => $fields,\n 'sorts' => [\n [\n 'propertyName' => 'lastmodifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n ];\n }\n\n public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array\n {\n $inputs = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $inputs[] = [\n 'from' => [\n 'id' => $crmId,\n ],\n 'to' => [\n 'id' => (string) $id,\n ],\n 'types' => [\n [\n 'associationCategory' => 'HUBSPOT_DEFINED',\n 'associationTypeId' => $associationType,\n ],\n ],\n ];\n }\n\n return ['inputs' => $inputs];\n }\n\n public function buildRemoveAssociationPayload(string $crmId, array $ids): array\n {\n $toArray = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $toArray[] = ['id' => (string) $id];\n }\n\n return [\n 'inputs' => [\n [\n 'from' => ['id' => $crmId],\n 'to' => $toArray,\n ],\n ],\n ];\n }\n\n public function addClosedStageFilters(array &$payload, array $closedStages): void\n {\n if (! empty($closedStages['won'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['won'],\n ];\n }\n\n if (! empty($closedStages['lost'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['lost'],\n ];\n }\n }\n\n public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void\n {\n $payload['filters'][] = [\n 'propertyName' => 'createdate',\n 'operator' => 'GT',\n 'value' => $createdAfter->getPreciseTimestamp(3),\n ];\n }\n\n public function getDealsInBulkPayload(array $dealIds): array\n {\n return [\n 'filterGroups' => [\n [\n 'filters' => [\n [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'IN',\n 'values' => $dealIds,\n ],\n ],\n ],\n ],\n 'limit' => self::MAX_FILTER_SIZE,\n ];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3653358008198671637
|
2079491755365055053
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] string $phone The phone number to search.
* @param bool $isAlternativeSearch Indicates if an alternative search should be performed
* if the first search fails to find a match. The first search request should cover most of the cases.
*/
public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array
{
$filterPropertyNames = $isAlternativeSearch ?
['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :
['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];
$filterGroups = array_map(function ($propertyName) use ($phone) {
return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);
}, $filterPropertyNames);
$properties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
return $this->createPayload($filterGroups, $properties);
}
/**
* Creates a filter group with the specified parameters.
*/
private function createFilterGroup(string $propertyName, string $operator, $value): array
{
return [
'filters' => [
[
'propertyName' => $propertyName,
'operator' => $operator,
'value' => $value,
],
],
];
}
/**
* Creates a payload with the specified filter groups and properties.
*/
private function createPayload(array $filterGroups, array $properties): array
{
return [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => 'modifieddate',
'direction' => 'DESCENDING',
],
],
'properties' => $properties,
'limit' => 1,
];
}
/**
* Generates a payload to find related activities based on the specified data and object type.
* The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)
* and are associated with any of the provided prospect types (contact or company).
*
* @param array $data An associative array containing the following keys:
* - 'from': The start time for the activity search (timestamp).
* - 'to': The end time for the activity search (timestamp).
* - 'contact': (optional) The ID of the associated contact.
* - 'company': (optional) The ID of the associated company.
* @param string $objectType The type of engagement to search for (meeting or other type like call).
*
* @return array The payload array to be used for searching related activities.
*/
public function getFindRelatedActivityPayload(array $data, string $objectType): array
{
$payload = [
'sorts' => [
[
'propertyName' => self::SORT_PROPERTY,
'direction' => 'DESCENDING',
],
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
if ($objectType === self::ENGAGEMENT_MEETINGS) {
$payload['properties'] = [
'hs_meeting_title',
'hs_meeting_outcome',
'hs_activity_type',
'hs_timestamp',
'hubspot_owner_id',
'hs_meeting_body',
'hs_internal_meeting_notes',
'hs_meeting_location',
'hs_meeting_start_time',
'hs_meeting_end_time',
];
} else {
$payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];
}
$timeFiltersWithEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'LTE',
'value' => $data['to'],
],
];
$timeFiltersWithoutEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'NOT_HAS_PROPERTY',
],
];
$payload['filterGroups'] = [];
$associationTypes = ['contact', 'company'];
foreach ($associationTypes as $type) {
if (! empty($data[$type])) {
$filterGroupWithEndTime = [
'filters' => array_merge(
$timeFiltersWithEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithEndTime;
$filterGroupWithoutEndTime = [
'filters' => array_merge(
$timeFiltersWithoutEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithoutEndTime ;
}
}
return $payload;
}
/** Parameters
* [
* 'accountId' => $crmAccountId,
* 'sortBy' => $sortBy,
* 'sortDir' => $sortDir,
* 'onlyOpen' => $onlyOpen,
* 'closedStages' => $this->getClosedDealStages(),
* 'userId' => $userId,
* ];
*/
public function generateOpportunitiesSearchPayload(
Configuration $config,
string $crmAccountId,
array $closedStages,
): array {
$closedFilters = [];
$filterGroups = [];
$onlyOpen = true;
switch ($config->getOpportunityAssignmentRule()) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$sortBy = 'createdate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$sortBy = 'createdate';
$sortDir = 'ASCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
default:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
$onlyOpen = false;
}
$baseFilters = [
[
'propertyName' => 'associations.company',
'operator' => 'EQ',
'value' => $crmAccountId,
],
];
// Handle closed stages in chunks
if ($onlyOpen) {
foreach (['won', 'lost'] as $key) {
if (! empty($closedStages[$key])) {
$chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);
foreach ($chunks as $chunk) {
$closedFilters[] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $chunk,
];
}
}
}
}
$filterGroups[] = [
'filters' => array_merge($baseFilters, $closedFilters),
];
$payload = [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => $sortBy,
'direction' => $sortDir,
],
],
'properties' => [
'dealname',
'amount',
'hubspot_owner_id',
'pipeline',
'dealstage',
'closedate',
'deal_currency_code',
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
$logger = app(LoggerInterface::class);
$logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
return $payload;
}
/**
* Converts v1 payload data to v3
*/
public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array
{
// Use this mapping until the whole Hubspot V1 is dropped.
$properties = [
'hubspot_owner_id' => $engagement['ownerId'],
'hs_timestamp' => $engagement['timestamp'],
];
// $engagement['activityType'] is $activity->category->name
if (isset($engagement['activityType'])) {
$properties['hs_activity_type'] = $engagement['activityType'];
}
$metadataKeyMap = [
'hs_meeting_outcome' => 'meetingOutcome',
'hs_meeting_title' => 'title',
'hs_meeting_start_time' => 'startTime',
'hs_meeting_end_time' => 'endTime',
'hs_meeting_body' => 'body',
'hs_internal_meeting_notes' => 'internalMeetingNotes',
];
foreach ($metadataKeyMap as $newKey => $oldKey) {
if (isset($metadata[$oldKey])) {
$properties[$newKey] = $metadata[$oldKey];
unset($metadata[$oldKey]);
}
}
$properties = [
...$properties,
...$metadata, // custom fields
];
$response = [
'properties' => $properties,
];
if (! empty($associations)) {
$response['associations'] = $associations;
}
return $response;
}
/**
* Generate a payload to search for contacts by name.
* The search is a token search in firstname or lastname
*/
public function generateSearchContactsByNamePayload(string $name, array $fields): array
{
$firstNameFilter = [
'propertyName' => 'firstname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
$lastNameFilter = [
'propertyName' => 'lastname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
return [
'filterGroups' => [
[
'filters' => [$firstNameFilter],
],
[
'filters' => [$lastNameFilter],
],
],
'properties' => $fields,
'sorts' => [
[
'propertyName' => 'lastmodifieddate',
'direction' => 'DESCENDING',
],
],
];
}
public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array
{
$inputs = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$inputs[] = [
'from' => [
'id' => $crmId,
],
'to' => [
'id' => (string) $id,
],
'types' => [
[
'associationCategory' => 'HUBSPOT_DEFINED',
'associationTypeId' => $associationType,
],
],
];
}
return ['inputs' => $inputs];
}
public function buildRemoveAssociationPayload(string $crmId, array $ids): array
{
$toArray = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$toArray[] = ['id' => (string) $id];
}
return [
'inputs' => [
[
'from' => ['id' => $crmId],
'to' => $toArray,
],
],
];
}
public function addClosedStageFilters(array &$payload, array $closedStages): void
{
if (! empty($closedStages['won'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['won'],
];
}
if (! empty($closedStages['lost'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['lost'],
];
}
}
public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void
{
$payload['filters'][] = [
'propertyName' => 'createdate',
'operator' => 'GT',
'value' => $createdAfter->getPreciseTimestamp(3),
];
}
public function getDealsInBulkPayload(array $dealIds): array
{
return [
'filterGroups' => [
[
'filters' => [
[
'propertyName' => 'hs_object_id',
'operator' => 'IN',
'values' => $dealIds,
],
],
],
],
'limit' => self::MAX_FILTER_SIZE,
];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4834
|
173
|
39
|
2026-05-07T14:31:57.327073+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164317327_m1.jpg...
|
PhpStorm
|
faVsco.js – PayloadBuilder.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] string $phone The phone number to search.
* @param bool $isAlternativeSearch Indicates if an alternative search should be performed
* if the first search fails to find a match. The first search request should cover most of the cases.
*/
public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array
{
$filterPropertyNames = $isAlternativeSearch ?
['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :
['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];
$filterGroups = array_map(function ($propertyName) use ($phone) {
return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);
}, $filterPropertyNames);
$properties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
return $this->createPayload($filterGroups, $properties);
}
/**
* Creates a filter group with the specified parameters.
*/
private function createFilterGroup(string $propertyName, string $operator, $value): array
{
return [
'filters' => [
[
'propertyName' => $propertyName,
'operator' => $operator,
'value' => $value,
],
],
];
}
/**
* Creates a payload with the specified filter groups and properties.
*/
private function createPayload(array $filterGroups, array $properties): array
{
return [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => 'modifieddate',
'direction' => 'DESCENDING',
],
],
'properties' => $properties,
'limit' => 1,
];
}
/**
* Generates a payload to find related activities based on the specified data and object type.
* The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)
* and are associated with any of the provided prospect types (contact or company).
*
* @param array $data An associative array containing the following keys:
* - 'from': The start time for the activity search (timestamp).
* - 'to': The end time for the activity search (timestamp).
* - 'contact': (optional) The ID of the associated contact.
* - 'company': (optional) The ID of the associated company.
* @param string $objectType The type of engagement to search for (meeting or other type like call).
*
* @return array The payload array to be used for searching related activities.
*/
public function getFindRelatedActivityPayload(array $data, string $objectType): array
{
$payload = [
'sorts' => [
[
'propertyName' => self::SORT_PROPERTY,
'direction' => 'DESCENDING',
],
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
if ($objectType === self::ENGAGEMENT_MEETINGS) {
$payload['properties'] = [
'hs_meeting_title',
'hs_meeting_outcome',
'hs_activity_type',
'hs_timestamp',
'hubspot_owner_id',
'hs_meeting_body',
'hs_internal_meeting_notes',
'hs_meeting_location',
'hs_meeting_start_time',
'hs_meeting_end_time',
];
} else {
$payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];
}
$timeFiltersWithEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'LTE',
'value' => $data['to'],
],
];
$timeFiltersWithoutEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'NOT_HAS_PROPERTY',
],
];
$payload['filterGroups'] = [];
$associationTypes = ['contact', 'company'];
foreach ($associationTypes as $type) {
if (! empty($data[$type])) {
$filterGroupWithEndTime = [
'filters' => array_merge(
$timeFiltersWithEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithEndTime;
$filterGroupWithoutEndTime = [
'filters' => array_merge(
$timeFiltersWithoutEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithoutEndTime ;
}
}
return $payload;
}
/** Parameters
* [
* 'accountId' => $crmAccountId,
* 'sortBy' => $sortBy,
* 'sortDir' => $sortDir,
* 'onlyOpen' => $onlyOpen,
* 'closedStages' => $this->getClosedDealStages(),
* 'userId' => $userId,
* ];
*/
public function generateOpportunitiesSearchPayload(
Configuration $config,
string $crmAccountId,
array $closedStages,
): array {
$closedFilters = [];
$filterGroups = [];
$onlyOpen = true;
switch ($config->getOpportunityAssignmentRule()) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$sortBy = 'createdate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$sortBy = 'createdate';
$sortDir = 'ASCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
default:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
$onlyOpen = false;
}
$baseFilters = [
[
'propertyName' => 'associations.company',
'operator' => 'EQ',
'value' => $crmAccountId,
],
];
// Handle closed stages in chunks
if ($onlyOpen) {
foreach (['won', 'lost'] as $key) {
if (! empty($closedStages[$key])) {
$chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);
foreach ($chunks as $chunk) {
$closedFilters[] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $chunk,
];
}
}
}
}
$filterGroups[] = [
'filters' => array_merge($baseFilters, $closedFilters),
];
$payload = [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => $sortBy,
'direction' => $sortDir,
],
],
'properties' => [
'dealname',
'amount',
'hubspot_owner_id',
'pipeline',
'dealstage',
'closedate',
'deal_currency_code',
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
$logger = app(LoggerInterface::class);
$logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
return $payload;
}
/**
* Converts v1 payload data to v3
*/
public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array
{
// Use this mapping until the whole Hubspot V1 is dropped.
$properties = [
'hubspot_owner_id' => $engagement['ownerId'],
'hs_timestamp' => $engagement['timestamp'],
];
// $engagement['activityType'] is $activity->category->name
if (isset($engagement['activityType'])) {
$properties['hs_activity_type'] = $engagement['activityType'];
}
$metadataKeyMap = [
'hs_meeting_outcome' => 'meetingOutcome',
'hs_meeting_title' => 'title',
'hs_meeting_start_time' => 'startTime',
'hs_meeting_end_time' => 'endTime',
'hs_meeting_body' => 'body',
'hs_internal_meeting_notes' => 'internalMeetingNotes',
];
foreach ($metadataKeyMap as $newKey => $oldKey) {
if (isset($metadata[$oldKey])) {
$properties[$newKey] = $metadata[$oldKey];
unset($metadata[$oldKey]);
}
}
$properties = [
...$properties,
...$metadata, // custom fields
];
$response = [
'properties' => $properties,
];
if (! empty($associations)) {
$response['associations'] = $associations;
}
return $response;
}
/**
* Generate a payload to search for contacts by name.
* The search is a token search in firstname or lastname
*/
public function generateSearchContactsByNamePayload(string $name, array $fields): array
{
$firstNameFilter = [
'propertyName' => 'firstname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
$lastNameFilter = [
'propertyName' => 'lastname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
return [
'filterGroups' => [
[
'filters' => [$firstNameFilter],
],
[
'filters' => [$lastNameFilter],
],
],
'properties' => $fields,
'sorts' => [
[
'propertyName' => 'lastmodifieddate',
'direction' => 'DESCENDING',
],
],
];
}
public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array
{
$inputs = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$inputs[] = [
'from' => [
'id' => $crmId,
],
'to' => [
'id' => (string) $id,
],
'types' => [
[
'associationCategory' => 'HUBSPOT_DEFINED',
'associationTypeId' => $associationType,
],
],
];
}
return ['inputs' => $inputs];
}
public function buildRemoveAssociationPayload(string $crmId, array $ids): array
{
$toArray = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$toArray[] = ['id' => (string) $id];
}
return [
'inputs' => [
[
'from' => ['id' => $crmId],
'to' => $toArray,
],
],
];
}
public function addClosedStageFilters(array &$payload, array $closedStages): void
{
if (! empty($closedStages['won'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['won'],
];
}
if (! empty($closedStages['lost'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['lost'],
];
}
}
public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void
{
$payload['filters'][] = [
'propertyName' => 'createdate',
'operator' => 'GT',
'value' => $createdAfter->getPreciseTimestamp(3),
];
}
public function getDealsInBulkPayload(array $dealIds): array
{
return [
'filterGroups' => [
[
'filters' => [
[
'propertyName' => 'hs_object_id',
'operator' => 'IN',
'values' => $dealIds,
],
],
],
],
'limit' => self::MAX_FILTER_SIZE,
];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"25","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Psr\\Log\\LoggerInterface;\n\nclass PayloadBuilder\n{\n public const int MAX_SEARCH_REQUEST_LIMIT = 200;\n\n private const int MAX_FILTER_SIZE = 100;\n\n private const string SORT_PROPERTY = 'hs_timestamp';\n private const string ENGAGEMENT_MEETINGS = 'meetings';\n\n public function getLinkToTaskPayload(string $objectType, string $objectId, string $engagementType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($engagementType === self::ENGAGEMENT_MEETINGS) {\n $payload['filterGroups'] = $this->buildMeetingFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_timestamp',\n 'hs_activity_type',\n ];\n } else {\n $payload['filterGroups'] = $this->buildTaskFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp'];\n }\n\n return $payload;\n }\n\n public function getRecentlyUpdatedSearchPayload(Carbon $since, ?Carbon $to, array $properties): array\n {\n $lastUpdateDate = in_array('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n public function getByProfileSearchPayload(string $crmId, array $properties, Carbon $since, ?Carbon $to): array\n {\n $lastUpdateDate = array_key_exists('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n [\n 'propertyName' => 'hubspot_owner_id',\n 'operator' => 'EQ',\n 'value' => $crmId,\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n private function buildMeetingFiltersForLinkToTask($objectType, $objectId): array\n {\n $filters = [];\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'IN',\n 'values' => ['SCHEDULED', 'RESCHEDULED'],\n ],\n ],\n ];\n\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ],\n ];\n\n return $filters;\n }\n\n private function buildTaskFiltersForLinkToTask($objectType, $objectId): array\n {\n return [\n [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n ],\n ],\n ];\n }\n\n private function getAssociatedObjectFilter(string $objectType, string $objectId): array\n {\n return [\n 'propertyName' => 'associations.' . $objectType,\n 'operator' => 'EQ',\n 'value' => $objectId,\n ];\n }\n\n public function generatePlaybackURLSearchPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n public function generateGetCallsPayload(Carbon $from, Carbon $to, string $activityProvider, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $activityProvider,\n ],\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function generateSearchCallsByPeriodPayload(Carbon $from, Carbon $to, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $this->getSearchCallAttributes(),\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function getSearchCallAttributes(): array\n {\n return [\n 'hs_timestamp',\n 'hs_call_recording_url',\n 'hs_call_body',\n 'hs_call_status',\n 'hs_call_to_number',\n 'hs_call_from_number',\n 'hs_call_duration',\n 'hs_call_disposition',\n 'hs_call_title',\n 'hs_call_direction',\n 'hubspot_owner_id',\n 'hs_activity_type',\n 'hs_call_external_id',\n 'hs_call_source',\n ];\n }\n\n public function generatePlaybackAddUrlBatchPayload(array $crmUpdateData): array\n {\n $updateObjectsData = [];\n\n foreach ($crmUpdateData as $data) {\n $updateObjectsData[] = [\n 'id' => $data['crm_id'],\n 'properties' => [\n 'hs_call_body' => $data['hs_call_body'] .\n '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">' .\n '<a href=\"' . $data['playback_url'] . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>',\n ],\n ];\n }\n\n return ['inputs' => $updateObjectsData];\n }\n\n public function generateSearchCallByTokenPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload for phone search based on the specified parameters.\n *\n * @param string $phone The phone number to search.\n * @param bool $isAlternativeSearch Indicates if an alternative search should be performed\n * if the first search fails to find a match. The first search request should cover most of the cases.\n */\n public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array\n {\n $filterPropertyNames = $isAlternativeSearch ?\n ['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :\n ['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];\n\n $filterGroups = array_map(function ($propertyName) use ($phone) {\n return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);\n }, $filterPropertyNames);\n\n $properties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n\n return $this->createPayload($filterGroups, $properties);\n }\n\n /**\n * Creates a filter group with the specified parameters.\n */\n private function createFilterGroup(string $propertyName, string $operator, $value): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => $propertyName,\n 'operator' => $operator,\n 'value' => $value,\n ],\n ],\n ];\n }\n\n /**\n * Creates a payload with the specified filter groups and properties.\n */\n private function createPayload(array $filterGroups, array $properties): array\n {\n return [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $properties,\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload to find related activities based on the specified data and object type.\n * The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)\n * and are associated with any of the provided prospect types (contact or company).\n *\n * @param array $data An associative array containing the following keys:\n * - 'from': The start time for the activity search (timestamp).\n * - 'to': The end time for the activity search (timestamp).\n * - 'contact': (optional) The ID of the associated contact.\n * - 'company': (optional) The ID of the associated company.\n * @param string $objectType The type of engagement to search for (meeting or other type like call).\n *\n * @return array The payload array to be used for searching related activities.\n */\n public function getFindRelatedActivityPayload(array $data, string $objectType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($objectType === self::ENGAGEMENT_MEETINGS) {\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_activity_type',\n 'hs_timestamp',\n 'hubspot_owner_id',\n 'hs_meeting_body',\n 'hs_internal_meeting_notes',\n 'hs_meeting_location',\n 'hs_meeting_start_time',\n 'hs_meeting_end_time',\n ];\n } else {\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];\n }\n\n $timeFiltersWithEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'LTE',\n 'value' => $data['to'],\n ],\n ];\n\n $timeFiltersWithoutEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ];\n\n $payload['filterGroups'] = [];\n\n $associationTypes = ['contact', 'company'];\n foreach ($associationTypes as $type) {\n if (! empty($data[$type])) {\n $filterGroupWithEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithEndTime;\n\n $filterGroupWithoutEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithoutEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithoutEndTime ;\n }\n }\n\n return $payload;\n }\n\n /** Parameters\n * [\n * 'accountId' => $crmAccountId,\n * 'sortBy' => $sortBy,\n * 'sortDir' => $sortDir,\n * 'onlyOpen' => $onlyOpen,\n * 'closedStages' => $this->getClosedDealStages(),\n * 'userId' => $userId,\n * ];\n */\n public function generateOpportunitiesSearchPayload(\n Configuration $config,\n string $crmAccountId,\n array $closedStages,\n ): array {\n $closedFilters = [];\n $filterGroups = [];\n $onlyOpen = true;\n\n switch ($config->getOpportunityAssignmentRule()) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'ASCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n default:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n $onlyOpen = false;\n }\n\n $baseFilters = [\n [\n 'propertyName' => 'associations.company',\n 'operator' => 'EQ',\n 'value' => $crmAccountId,\n ],\n ];\n\n // Handle closed stages in chunks\n if ($onlyOpen) {\n foreach (['won', 'lost'] as $key) {\n if (! empty($closedStages[$key])) {\n $chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);\n foreach ($chunks as $chunk) {\n $closedFilters[] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $chunk,\n ];\n }\n }\n }\n }\n\n $filterGroups[] = [\n 'filters' => array_merge($baseFilters, $closedFilters),\n ];\n\n $payload = [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => $sortBy,\n 'direction' => $sortDir,\n ],\n ],\n 'properties' => [\n 'dealname',\n 'amount',\n 'hubspot_owner_id',\n 'pipeline',\n 'dealstage',\n 'closedate',\n 'deal_currency_code',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n $logger = app(LoggerInterface::class);\n $logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * Converts v1 payload data to v3\n */\n public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array\n {\n // Use this mapping until the whole Hubspot V1 is dropped.\n $properties = [\n 'hubspot_owner_id' => $engagement['ownerId'],\n 'hs_timestamp' => $engagement['timestamp'],\n ];\n\n // $engagement['activityType'] is $activity->category->name\n if (isset($engagement['activityType'])) {\n $properties['hs_activity_type'] = $engagement['activityType'];\n }\n\n $metadataKeyMap = [\n 'hs_meeting_outcome' => 'meetingOutcome',\n 'hs_meeting_title' => 'title',\n 'hs_meeting_start_time' => 'startTime',\n 'hs_meeting_end_time' => 'endTime',\n 'hs_meeting_body' => 'body',\n 'hs_internal_meeting_notes' => 'internalMeetingNotes',\n ];\n\n foreach ($metadataKeyMap as $newKey => $oldKey) {\n if (isset($metadata[$oldKey])) {\n $properties[$newKey] = $metadata[$oldKey];\n unset($metadata[$oldKey]);\n }\n }\n\n $properties = [\n ...$properties,\n ...$metadata, // custom fields\n ];\n\n $response = [\n 'properties' => $properties,\n ];\n\n if (! empty($associations)) {\n $response['associations'] = $associations;\n }\n\n return $response;\n }\n\n /**\n * Generate a payload to search for contacts by name.\n * The search is a token search in firstname or lastname\n */\n public function generateSearchContactsByNamePayload(string $name, array $fields): array\n {\n $firstNameFilter = [\n 'propertyName' => 'firstname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n $lastNameFilter = [\n 'propertyName' => 'lastname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n return [\n 'filterGroups' => [\n [\n 'filters' => [$firstNameFilter],\n ],\n [\n 'filters' => [$lastNameFilter],\n ],\n ],\n 'properties' => $fields,\n 'sorts' => [\n [\n 'propertyName' => 'lastmodifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n ];\n }\n\n public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array\n {\n $inputs = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $inputs[] = [\n 'from' => [\n 'id' => $crmId,\n ],\n 'to' => [\n 'id' => (string) $id,\n ],\n 'types' => [\n [\n 'associationCategory' => 'HUBSPOT_DEFINED',\n 'associationTypeId' => $associationType,\n ],\n ],\n ];\n }\n\n return ['inputs' => $inputs];\n }\n\n public function buildRemoveAssociationPayload(string $crmId, array $ids): array\n {\n $toArray = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $toArray[] = ['id' => (string) $id];\n }\n\n return [\n 'inputs' => [\n [\n 'from' => ['id' => $crmId],\n 'to' => $toArray,\n ],\n ],\n ];\n }\n\n public function addClosedStageFilters(array &$payload, array $closedStages): void\n {\n if (! empty($closedStages['won'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['won'],\n ];\n }\n\n if (! empty($closedStages['lost'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['lost'],\n ];\n }\n }\n\n public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void\n {\n $payload['filters'][] = [\n 'propertyName' => 'createdate',\n 'operator' => 'GT',\n 'value' => $createdAfter->getPreciseTimestamp(3),\n ];\n }\n\n public function getDealsInBulkPayload(array $dealIds): array\n {\n return [\n 'filterGroups' => [\n [\n 'filters' => [\n [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'IN',\n 'values' => $dealIds,\n ],\n ],\n ],\n ],\n 'limit' => self::MAX_FILTER_SIZE,\n ];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Psr\\Log\\LoggerInterface;\n\nclass PayloadBuilder\n{\n public const int MAX_SEARCH_REQUEST_LIMIT = 200;\n\n private const int MAX_FILTER_SIZE = 100;\n\n private const string SORT_PROPERTY = 'hs_timestamp';\n private const string ENGAGEMENT_MEETINGS = 'meetings';\n\n public function getLinkToTaskPayload(string $objectType, string $objectId, string $engagementType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($engagementType === self::ENGAGEMENT_MEETINGS) {\n $payload['filterGroups'] = $this->buildMeetingFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_timestamp',\n 'hs_activity_type',\n ];\n } else {\n $payload['filterGroups'] = $this->buildTaskFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp'];\n }\n\n return $payload;\n }\n\n public function getRecentlyUpdatedSearchPayload(Carbon $since, ?Carbon $to, array $properties): array\n {\n $lastUpdateDate = in_array('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n public function getByProfileSearchPayload(string $crmId, array $properties, Carbon $since, ?Carbon $to): array\n {\n $lastUpdateDate = array_key_exists('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n [\n 'propertyName' => 'hubspot_owner_id',\n 'operator' => 'EQ',\n 'value' => $crmId,\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n private function buildMeetingFiltersForLinkToTask($objectType, $objectId): array\n {\n $filters = [];\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'IN',\n 'values' => ['SCHEDULED', 'RESCHEDULED'],\n ],\n ],\n ];\n\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ],\n ];\n\n return $filters;\n }\n\n private function buildTaskFiltersForLinkToTask($objectType, $objectId): array\n {\n return [\n [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n ],\n ],\n ];\n }\n\n private function getAssociatedObjectFilter(string $objectType, string $objectId): array\n {\n return [\n 'propertyName' => 'associations.' . $objectType,\n 'operator' => 'EQ',\n 'value' => $objectId,\n ];\n }\n\n public function generatePlaybackURLSearchPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n public function generateGetCallsPayload(Carbon $from, Carbon $to, string $activityProvider, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $activityProvider,\n ],\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function generateSearchCallsByPeriodPayload(Carbon $from, Carbon $to, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $this->getSearchCallAttributes(),\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function getSearchCallAttributes(): array\n {\n return [\n 'hs_timestamp',\n 'hs_call_recording_url',\n 'hs_call_body',\n 'hs_call_status',\n 'hs_call_to_number',\n 'hs_call_from_number',\n 'hs_call_duration',\n 'hs_call_disposition',\n 'hs_call_title',\n 'hs_call_direction',\n 'hubspot_owner_id',\n 'hs_activity_type',\n 'hs_call_external_id',\n 'hs_call_source',\n ];\n }\n\n public function generatePlaybackAddUrlBatchPayload(array $crmUpdateData): array\n {\n $updateObjectsData = [];\n\n foreach ($crmUpdateData as $data) {\n $updateObjectsData[] = [\n 'id' => $data['crm_id'],\n 'properties' => [\n 'hs_call_body' => $data['hs_call_body'] .\n '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">' .\n '<a href=\"' . $data['playback_url'] . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>',\n ],\n ];\n }\n\n return ['inputs' => $updateObjectsData];\n }\n\n public function generateSearchCallByTokenPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload for phone search based on the specified parameters.\n *\n * @param string $phone The phone number to search.\n * @param bool $isAlternativeSearch Indicates if an alternative search should be performed\n * if the first search fails to find a match. The first search request should cover most of the cases.\n */\n public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array\n {\n $filterPropertyNames = $isAlternativeSearch ?\n ['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :\n ['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];\n\n $filterGroups = array_map(function ($propertyName) use ($phone) {\n return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);\n }, $filterPropertyNames);\n\n $properties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n\n return $this->createPayload($filterGroups, $properties);\n }\n\n /**\n * Creates a filter group with the specified parameters.\n */\n private function createFilterGroup(string $propertyName, string $operator, $value): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => $propertyName,\n 'operator' => $operator,\n 'value' => $value,\n ],\n ],\n ];\n }\n\n /**\n * Creates a payload with the specified filter groups and properties.\n */\n private function createPayload(array $filterGroups, array $properties): array\n {\n return [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $properties,\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload to find related activities based on the specified data and object type.\n * The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)\n * and are associated with any of the provided prospect types (contact or company).\n *\n * @param array $data An associative array containing the following keys:\n * - 'from': The start time for the activity search (timestamp).\n * - 'to': The end time for the activity search (timestamp).\n * - 'contact': (optional) The ID of the associated contact.\n * - 'company': (optional) The ID of the associated company.\n * @param string $objectType The type of engagement to search for (meeting or other type like call).\n *\n * @return array The payload array to be used for searching related activities.\n */\n public function getFindRelatedActivityPayload(array $data, string $objectType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($objectType === self::ENGAGEMENT_MEETINGS) {\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_activity_type',\n 'hs_timestamp',\n 'hubspot_owner_id',\n 'hs_meeting_body',\n 'hs_internal_meeting_notes',\n 'hs_meeting_location',\n 'hs_meeting_start_time',\n 'hs_meeting_end_time',\n ];\n } else {\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];\n }\n\n $timeFiltersWithEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'LTE',\n 'value' => $data['to'],\n ],\n ];\n\n $timeFiltersWithoutEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ];\n\n $payload['filterGroups'] = [];\n\n $associationTypes = ['contact', 'company'];\n foreach ($associationTypes as $type) {\n if (! empty($data[$type])) {\n $filterGroupWithEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithEndTime;\n\n $filterGroupWithoutEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithoutEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithoutEndTime ;\n }\n }\n\n return $payload;\n }\n\n /** Parameters\n * [\n * 'accountId' => $crmAccountId,\n * 'sortBy' => $sortBy,\n * 'sortDir' => $sortDir,\n * 'onlyOpen' => $onlyOpen,\n * 'closedStages' => $this->getClosedDealStages(),\n * 'userId' => $userId,\n * ];\n */\n public function generateOpportunitiesSearchPayload(\n Configuration $config,\n string $crmAccountId,\n array $closedStages,\n ): array {\n $closedFilters = [];\n $filterGroups = [];\n $onlyOpen = true;\n\n switch ($config->getOpportunityAssignmentRule()) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'ASCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n default:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n $onlyOpen = false;\n }\n\n $baseFilters = [\n [\n 'propertyName' => 'associations.company',\n 'operator' => 'EQ',\n 'value' => $crmAccountId,\n ],\n ];\n\n // Handle closed stages in chunks\n if ($onlyOpen) {\n foreach (['won', 'lost'] as $key) {\n if (! empty($closedStages[$key])) {\n $chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);\n foreach ($chunks as $chunk) {\n $closedFilters[] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $chunk,\n ];\n }\n }\n }\n }\n\n $filterGroups[] = [\n 'filters' => array_merge($baseFilters, $closedFilters),\n ];\n\n $payload = [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => $sortBy,\n 'direction' => $sortDir,\n ],\n ],\n 'properties' => [\n 'dealname',\n 'amount',\n 'hubspot_owner_id',\n 'pipeline',\n 'dealstage',\n 'closedate',\n 'deal_currency_code',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n $logger = app(LoggerInterface::class);\n $logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * Converts v1 payload data to v3\n */\n public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array\n {\n // Use this mapping until the whole Hubspot V1 is dropped.\n $properties = [\n 'hubspot_owner_id' => $engagement['ownerId'],\n 'hs_timestamp' => $engagement['timestamp'],\n ];\n\n // $engagement['activityType'] is $activity->category->name\n if (isset($engagement['activityType'])) {\n $properties['hs_activity_type'] = $engagement['activityType'];\n }\n\n $metadataKeyMap = [\n 'hs_meeting_outcome' => 'meetingOutcome',\n 'hs_meeting_title' => 'title',\n 'hs_meeting_start_time' => 'startTime',\n 'hs_meeting_end_time' => 'endTime',\n 'hs_meeting_body' => 'body',\n 'hs_internal_meeting_notes' => 'internalMeetingNotes',\n ];\n\n foreach ($metadataKeyMap as $newKey => $oldKey) {\n if (isset($metadata[$oldKey])) {\n $properties[$newKey] = $metadata[$oldKey];\n unset($metadata[$oldKey]);\n }\n }\n\n $properties = [\n ...$properties,\n ...$metadata, // custom fields\n ];\n\n $response = [\n 'properties' => $properties,\n ];\n\n if (! empty($associations)) {\n $response['associations'] = $associations;\n }\n\n return $response;\n }\n\n /**\n * Generate a payload to search for contacts by name.\n * The search is a token search in firstname or lastname\n */\n public function generateSearchContactsByNamePayload(string $name, array $fields): array\n {\n $firstNameFilter = [\n 'propertyName' => 'firstname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n $lastNameFilter = [\n 'propertyName' => 'lastname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n return [\n 'filterGroups' => [\n [\n 'filters' => [$firstNameFilter],\n ],\n [\n 'filters' => [$lastNameFilter],\n ],\n ],\n 'properties' => $fields,\n 'sorts' => [\n [\n 'propertyName' => 'lastmodifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n ];\n }\n\n public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array\n {\n $inputs = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $inputs[] = [\n 'from' => [\n 'id' => $crmId,\n ],\n 'to' => [\n 'id' => (string) $id,\n ],\n 'types' => [\n [\n 'associationCategory' => 'HUBSPOT_DEFINED',\n 'associationTypeId' => $associationType,\n ],\n ],\n ];\n }\n\n return ['inputs' => $inputs];\n }\n\n public function buildRemoveAssociationPayload(string $crmId, array $ids): array\n {\n $toArray = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $toArray[] = ['id' => (string) $id];\n }\n\n return [\n 'inputs' => [\n [\n 'from' => ['id' => $crmId],\n 'to' => $toArray,\n ],\n ],\n ];\n }\n\n public function addClosedStageFilters(array &$payload, array $closedStages): void\n {\n if (! empty($closedStages['won'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['won'],\n ];\n }\n\n if (! empty($closedStages['lost'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['lost'],\n ];\n }\n }\n\n public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void\n {\n $payload['filters'][] = [\n 'propertyName' => 'createdate',\n 'operator' => 'GT',\n 'value' => $createdAfter->getPreciseTimestamp(3),\n ];\n }\n\n public function getDealsInBulkPayload(array $dealIds): array\n {\n return [\n 'filterGroups' => [\n [\n 'filters' => [\n [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'IN',\n 'values' => $dealIds,\n ],\n ],\n ],\n ],\n 'limit' => self::MAX_FILTER_SIZE,\n ];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3653358008198671637
|
2079491755365055053
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] string $phone The phone number to search.
* @param bool $isAlternativeSearch Indicates if an alternative search should be performed
* if the first search fails to find a match. The first search request should cover most of the cases.
*/
public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array
{
$filterPropertyNames = $isAlternativeSearch ?
['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :
['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];
$filterGroups = array_map(function ($propertyName) use ($phone) {
return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);
}, $filterPropertyNames);
$properties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
return $this->createPayload($filterGroups, $properties);
}
/**
* Creates a filter group with the specified parameters.
*/
private function createFilterGroup(string $propertyName, string $operator, $value): array
{
return [
'filters' => [
[
'propertyName' => $propertyName,
'operator' => $operator,
'value' => $value,
],
],
];
}
/**
* Creates a payload with the specified filter groups and properties.
*/
private function createPayload(array $filterGroups, array $properties): array
{
return [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => 'modifieddate',
'direction' => 'DESCENDING',
],
],
'properties' => $properties,
'limit' => 1,
];
}
/**
* Generates a payload to find related activities based on the specified data and object type.
* The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)
* and are associated with any of the provided prospect types (contact or company).
*
* @param array $data An associative array containing the following keys:
* - 'from': The start time for the activity search (timestamp).
* - 'to': The end time for the activity search (timestamp).
* - 'contact': (optional) The ID of the associated contact.
* - 'company': (optional) The ID of the associated company.
* @param string $objectType The type of engagement to search for (meeting or other type like call).
*
* @return array The payload array to be used for searching related activities.
*/
public function getFindRelatedActivityPayload(array $data, string $objectType): array
{
$payload = [
'sorts' => [
[
'propertyName' => self::SORT_PROPERTY,
'direction' => 'DESCENDING',
],
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
if ($objectType === self::ENGAGEMENT_MEETINGS) {
$payload['properties'] = [
'hs_meeting_title',
'hs_meeting_outcome',
'hs_activity_type',
'hs_timestamp',
'hubspot_owner_id',
'hs_meeting_body',
'hs_internal_meeting_notes',
'hs_meeting_location',
'hs_meeting_start_time',
'hs_meeting_end_time',
];
} else {
$payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];
}
$timeFiltersWithEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'LTE',
'value' => $data['to'],
],
];
$timeFiltersWithoutEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'NOT_HAS_PROPERTY',
],
];
$payload['filterGroups'] = [];
$associationTypes = ['contact', 'company'];
foreach ($associationTypes as $type) {
if (! empty($data[$type])) {
$filterGroupWithEndTime = [
'filters' => array_merge(
$timeFiltersWithEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithEndTime;
$filterGroupWithoutEndTime = [
'filters' => array_merge(
$timeFiltersWithoutEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithoutEndTime ;
}
}
return $payload;
}
/** Parameters
* [
* 'accountId' => $crmAccountId,
* 'sortBy' => $sortBy,
* 'sortDir' => $sortDir,
* 'onlyOpen' => $onlyOpen,
* 'closedStages' => $this->getClosedDealStages(),
* 'userId' => $userId,
* ];
*/
public function generateOpportunitiesSearchPayload(
Configuration $config,
string $crmAccountId,
array $closedStages,
): array {
$closedFilters = [];
$filterGroups = [];
$onlyOpen = true;
switch ($config->getOpportunityAssignmentRule()) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$sortBy = 'createdate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$sortBy = 'createdate';
$sortDir = 'ASCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
default:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
$onlyOpen = false;
}
$baseFilters = [
[
'propertyName' => 'associations.company',
'operator' => 'EQ',
'value' => $crmAccountId,
],
];
// Handle closed stages in chunks
if ($onlyOpen) {
foreach (['won', 'lost'] as $key) {
if (! empty($closedStages[$key])) {
$chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);
foreach ($chunks as $chunk) {
$closedFilters[] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $chunk,
];
}
}
}
}
$filterGroups[] = [
'filters' => array_merge($baseFilters, $closedFilters),
];
$payload = [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => $sortBy,
'direction' => $sortDir,
],
],
'properties' => [
'dealname',
'amount',
'hubspot_owner_id',
'pipeline',
'dealstage',
'closedate',
'deal_currency_code',
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
$logger = app(LoggerInterface::class);
$logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
return $payload;
}
/**
* Converts v1 payload data to v3
*/
public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array
{
// Use this mapping until the whole Hubspot V1 is dropped.
$properties = [
'hubspot_owner_id' => $engagement['ownerId'],
'hs_timestamp' => $engagement['timestamp'],
];
// $engagement['activityType'] is $activity->category->name
if (isset($engagement['activityType'])) {
$properties['hs_activity_type'] = $engagement['activityType'];
}
$metadataKeyMap = [
'hs_meeting_outcome' => 'meetingOutcome',
'hs_meeting_title' => 'title',
'hs_meeting_start_time' => 'startTime',
'hs_meeting_end_time' => 'endTime',
'hs_meeting_body' => 'body',
'hs_internal_meeting_notes' => 'internalMeetingNotes',
];
foreach ($metadataKeyMap as $newKey => $oldKey) {
if (isset($metadata[$oldKey])) {
$properties[$newKey] = $metadata[$oldKey];
unset($metadata[$oldKey]);
}
}
$properties = [
...$properties,
...$metadata, // custom fields
];
$response = [
'properties' => $properties,
];
if (! empty($associations)) {
$response['associations'] = $associations;
}
return $response;
}
/**
* Generate a payload to search for contacts by name.
* The search is a token search in firstname or lastname
*/
public function generateSearchContactsByNamePayload(string $name, array $fields): array
{
$firstNameFilter = [
'propertyName' => 'firstname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
$lastNameFilter = [
'propertyName' => 'lastname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
return [
'filterGroups' => [
[
'filters' => [$firstNameFilter],
],
[
'filters' => [$lastNameFilter],
],
],
'properties' => $fields,
'sorts' => [
[
'propertyName' => 'lastmodifieddate',
'direction' => 'DESCENDING',
],
],
];
}
public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array
{
$inputs = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$inputs[] = [
'from' => [
'id' => $crmId,
],
'to' => [
'id' => (string) $id,
],
'types' => [
[
'associationCategory' => 'HUBSPOT_DEFINED',
'associationTypeId' => $associationType,
],
],
];
}
return ['inputs' => $inputs];
}
public function buildRemoveAssociationPayload(string $crmId, array $ids): array
{
$toArray = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$toArray[] = ['id' => (string) $id];
}
return [
'inputs' => [
[
'from' => ['id' => $crmId],
'to' => $toArray,
],
],
];
}
public function addClosedStageFilters(array &$payload, array $closedStages): void
{
if (! empty($closedStages['won'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['won'],
];
}
if (! empty($closedStages['lost'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['lost'],
];
}
}
public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void
{
$payload['filters'][] = [
'propertyName' => 'createdate',
'operator' => 'GT',
'value' => $createdAfter->getPreciseTimestamp(3),
];
}
public function getDealsInBulkPayload(array $dealIds): array
{
return [
'filterGroups' => [
[
'filters' => [
[
'propertyName' => 'hs_object_id',
'operator' => 'IN',
'values' => $dealIds,
],
],
],
],
'limit' => self::MAX_FILTER_SIZE,
];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
4830
|
NULL
|
NULL
|
NULL
|
|
4835
|
174
|
29
|
2026-05-07T14:31:57.645168+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164317645_m2.jpg...
|
PhpStorm
|
faVsco.js – PayloadBuilder.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] string $phone The phone number to search.
* @param bool $isAlternativeSearch Indicates if an alternative search should be performed
* if the first search fails to find a match. The first search request should cover most of the cases.
*/
public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array
{
$filterPropertyNames = $isAlternativeSearch ?
['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :
['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];
$filterGroups = array_map(function ($propertyName) use ($phone) {
return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);
}, $filterPropertyNames);
$properties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
return $this->createPayload($filterGroups, $properties);
}
/**
* Creates a filter group with the specified parameters.
*/
private function createFilterGroup(string $propertyName, string $operator, $value): array
{
return [
'filters' => [
[
'propertyName' => $propertyName,
'operator' => $operator,
'value' => $value,
],
],
];
}
/**
* Creates a payload with the specified filter groups and properties.
*/
private function createPayload(array $filterGroups, array $properties): array
{
return [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => 'modifieddate',
'direction' => 'DESCENDING',
],
],
'properties' => $properties,
'limit' => 1,
];
}
/**
* Generates a payload to find related activities based on the specified data and object type.
* The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)
* and are associated with any of the provided prospect types (contact or company).
*
* @param array $data An associative array containing the following keys:
* - 'from': The start time for the activity search (timestamp).
* - 'to': The end time for the activity search (timestamp).
* - 'contact': (optional) The ID of the associated contact.
* - 'company': (optional) The ID of the associated company.
* @param string $objectType The type of engagement to search for (meeting or other type like call).
*
* @return array The payload array to be used for searching related activities.
*/
public function getFindRelatedActivityPayload(array $data, string $objectType): array
{
$payload = [
'sorts' => [
[
'propertyName' => self::SORT_PROPERTY,
'direction' => 'DESCENDING',
],
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
if ($objectType === self::ENGAGEMENT_MEETINGS) {
$payload['properties'] = [
'hs_meeting_title',
'hs_meeting_outcome',
'hs_activity_type',
'hs_timestamp',
'hubspot_owner_id',
'hs_meeting_body',
'hs_internal_meeting_notes',
'hs_meeting_location',
'hs_meeting_start_time',
'hs_meeting_end_time',
];
} else {
$payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];
}
$timeFiltersWithEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'LTE',
'value' => $data['to'],
],
];
$timeFiltersWithoutEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'NOT_HAS_PROPERTY',
],
];
$payload['filterGroups'] = [];
$associationTypes = ['contact', 'company'];
foreach ($associationTypes as $type) {
if (! empty($data[$type])) {
$filterGroupWithEndTime = [
'filters' => array_merge(
$timeFiltersWithEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithEndTime;
$filterGroupWithoutEndTime = [
'filters' => array_merge(
$timeFiltersWithoutEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithoutEndTime ;
}
}
return $payload;
}
/** Parameters
* [
* 'accountId' => $crmAccountId,
* 'sortBy' => $sortBy,
* 'sortDir' => $sortDir,
* 'onlyOpen' => $onlyOpen,
* 'closedStages' => $this->getClosedDealStages(),
* 'userId' => $userId,
* ];
*/
public function generateOpportunitiesSearchPayload(
Configuration $config,
string $crmAccountId,
array $closedStages,
): array {
$closedFilters = [];
$filterGroups = [];
$onlyOpen = true;
switch ($config->getOpportunityAssignmentRule()) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$sortBy = 'createdate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$sortBy = 'createdate';
$sortDir = 'ASCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
default:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
$onlyOpen = false;
}
$baseFilters = [
[
'propertyName' => 'associations.company',
'operator' => 'EQ',
'value' => $crmAccountId,
],
];
// Handle closed stages in chunks
if ($onlyOpen) {
foreach (['won', 'lost'] as $key) {
if (! empty($closedStages[$key])) {
$chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);
foreach ($chunks as $chunk) {
$closedFilters[] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $chunk,
];
}
}
}
}
$filterGroups[] = [
'filters' => array_merge($baseFilters, $closedFilters),
];
$payload = [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => $sortBy,
'direction' => $sortDir,
],
],
'properties' => [
'dealname',
'amount',
'hubspot_owner_id',
'pipeline',
'dealstage',
'closedate',
'deal_currency_code',
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
$logger = app(LoggerInterface::class);
$logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
return $payload;
}
/**
* Converts v1 payload data to v3
*/
public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array
{
// Use this mapping until the whole Hubspot V1 is dropped.
$properties = [
'hubspot_owner_id' => $engagement['ownerId'],
'hs_timestamp' => $engagement['timestamp'],
];
// $engagement['activityType'] is $activity->category->name
if (isset($engagement['activityType'])) {
$properties['hs_activity_type'] = $engagement['activityType'];
}
$metadataKeyMap = [
'hs_meeting_outcome' => 'meetingOutcome',
'hs_meeting_title' => 'title',
'hs_meeting_start_time' => 'startTime',
'hs_meeting_end_time' => 'endTime',
'hs_meeting_body' => 'body',
'hs_internal_meeting_notes' => 'internalMeetingNotes',
];
foreach ($metadataKeyMap as $newKey => $oldKey) {
if (isset($metadata[$oldKey])) {
$properties[$newKey] = $metadata[$oldKey];
unset($metadata[$oldKey]);
}
}
$properties = [
...$properties,
...$metadata, // custom fields
];
$response = [
'properties' => $properties,
];
if (! empty($associations)) {
$response['associations'] = $associations;
}
return $response;
}
/**
* Generate a payload to search for contacts by name.
* The search is a token search in firstname or lastname
*/
public function generateSearchContactsByNamePayload(string $name, array $fields): array
{
$firstNameFilter = [
'propertyName' => 'firstname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
$lastNameFilter = [
'propertyName' => 'lastname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
return [
'filterGroups' => [
[
'filters' => [$firstNameFilter],
],
[
'filters' => [$lastNameFilter],
],
],
'properties' => $fields,
'sorts' => [
[
'propertyName' => 'lastmodifieddate',
'direction' => 'DESCENDING',
],
],
];
}
public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array
{
$inputs = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$inputs[] = [
'from' => [
'id' => $crmId,
],
'to' => [
'id' => (string) $id,
],
'types' => [
[
'associationCategory' => 'HUBSPOT_DEFINED',
'associationTypeId' => $associationType,
],
],
];
}
return ['inputs' => $inputs];
}
public function buildRemoveAssociationPayload(string $crmId, array $ids): array
{
$toArray = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$toArray[] = ['id' => (string) $id];
}
return [
'inputs' => [
[
'from' => ['id' => $crmId],
'to' => $toArray,
],
],
];
}
public function addClosedStageFilters(array &$payload, array $closedStages): void
{
if (! empty($closedStages['won'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['won'],
];
}
if (! empty($closedStages['lost'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['lost'],
];
}
}
public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void
{
$payload['filters'][] = [
'propertyName' => 'createdate',
'operator' => 'GT',
'value' => $createdAfter->getPreciseTimestamp(3),
];
}
public function getDealsInBulkPayload(array $dealIds): array
{
return [
'filterGroups' => [
[
'filters' => [
[
'propertyName' => 'hs_object_id',
'operator' => 'IN',
'values' => $dealIds,
],
],
],
],
'limit' => self::MAX_FILTER_SIZE,
];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.39660904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.40525267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4162234,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.42486703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.43351063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.44448137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4554521,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.4820479,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.49301863,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.6821808,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.66855055,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.67852396,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6878325,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6974734,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.70478725,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.35904256,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"25","depth":4,"bounds":{"left":0.36901596,"top":0.2490024,"width":0.009973404,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.3806516,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.3879654,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Psr\\Log\\LoggerInterface;\n\nclass PayloadBuilder\n{\n public const int MAX_SEARCH_REQUEST_LIMIT = 200;\n\n private const int MAX_FILTER_SIZE = 100;\n\n private const string SORT_PROPERTY = 'hs_timestamp';\n private const string ENGAGEMENT_MEETINGS = 'meetings';\n\n public function getLinkToTaskPayload(string $objectType, string $objectId, string $engagementType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($engagementType === self::ENGAGEMENT_MEETINGS) {\n $payload['filterGroups'] = $this->buildMeetingFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_timestamp',\n 'hs_activity_type',\n ];\n } else {\n $payload['filterGroups'] = $this->buildTaskFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp'];\n }\n\n return $payload;\n }\n\n public function getRecentlyUpdatedSearchPayload(Carbon $since, ?Carbon $to, array $properties): array\n {\n $lastUpdateDate = in_array('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n public function getByProfileSearchPayload(string $crmId, array $properties, Carbon $since, ?Carbon $to): array\n {\n $lastUpdateDate = array_key_exists('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n [\n 'propertyName' => 'hubspot_owner_id',\n 'operator' => 'EQ',\n 'value' => $crmId,\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n private function buildMeetingFiltersForLinkToTask($objectType, $objectId): array\n {\n $filters = [];\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'IN',\n 'values' => ['SCHEDULED', 'RESCHEDULED'],\n ],\n ],\n ];\n\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ],\n ];\n\n return $filters;\n }\n\n private function buildTaskFiltersForLinkToTask($objectType, $objectId): array\n {\n return [\n [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n ],\n ],\n ];\n }\n\n private function getAssociatedObjectFilter(string $objectType, string $objectId): array\n {\n return [\n 'propertyName' => 'associations.' . $objectType,\n 'operator' => 'EQ',\n 'value' => $objectId,\n ];\n }\n\n public function generatePlaybackURLSearchPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n public function generateGetCallsPayload(Carbon $from, Carbon $to, string $activityProvider, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $activityProvider,\n ],\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function generateSearchCallsByPeriodPayload(Carbon $from, Carbon $to, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $this->getSearchCallAttributes(),\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function getSearchCallAttributes(): array\n {\n return [\n 'hs_timestamp',\n 'hs_call_recording_url',\n 'hs_call_body',\n 'hs_call_status',\n 'hs_call_to_number',\n 'hs_call_from_number',\n 'hs_call_duration',\n 'hs_call_disposition',\n 'hs_call_title',\n 'hs_call_direction',\n 'hubspot_owner_id',\n 'hs_activity_type',\n 'hs_call_external_id',\n 'hs_call_source',\n ];\n }\n\n public function generatePlaybackAddUrlBatchPayload(array $crmUpdateData): array\n {\n $updateObjectsData = [];\n\n foreach ($crmUpdateData as $data) {\n $updateObjectsData[] = [\n 'id' => $data['crm_id'],\n 'properties' => [\n 'hs_call_body' => $data['hs_call_body'] .\n '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">' .\n '<a href=\"' . $data['playback_url'] . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>',\n ],\n ];\n }\n\n return ['inputs' => $updateObjectsData];\n }\n\n public function generateSearchCallByTokenPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload for phone search based on the specified parameters.\n *\n * @param string $phone The phone number to search.\n * @param bool $isAlternativeSearch Indicates if an alternative search should be performed\n * if the first search fails to find a match. The first search request should cover most of the cases.\n */\n public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array\n {\n $filterPropertyNames = $isAlternativeSearch ?\n ['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :\n ['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];\n\n $filterGroups = array_map(function ($propertyName) use ($phone) {\n return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);\n }, $filterPropertyNames);\n\n $properties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n\n return $this->createPayload($filterGroups, $properties);\n }\n\n /**\n * Creates a filter group with the specified parameters.\n */\n private function createFilterGroup(string $propertyName, string $operator, $value): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => $propertyName,\n 'operator' => $operator,\n 'value' => $value,\n ],\n ],\n ];\n }\n\n /**\n * Creates a payload with the specified filter groups and properties.\n */\n private function createPayload(array $filterGroups, array $properties): array\n {\n return [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $properties,\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload to find related activities based on the specified data and object type.\n * The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)\n * and are associated with any of the provided prospect types (contact or company).\n *\n * @param array $data An associative array containing the following keys:\n * - 'from': The start time for the activity search (timestamp).\n * - 'to': The end time for the activity search (timestamp).\n * - 'contact': (optional) The ID of the associated contact.\n * - 'company': (optional) The ID of the associated company.\n * @param string $objectType The type of engagement to search for (meeting or other type like call).\n *\n * @return array The payload array to be used for searching related activities.\n */\n public function getFindRelatedActivityPayload(array $data, string $objectType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($objectType === self::ENGAGEMENT_MEETINGS) {\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_activity_type',\n 'hs_timestamp',\n 'hubspot_owner_id',\n 'hs_meeting_body',\n 'hs_internal_meeting_notes',\n 'hs_meeting_location',\n 'hs_meeting_start_time',\n 'hs_meeting_end_time',\n ];\n } else {\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];\n }\n\n $timeFiltersWithEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'LTE',\n 'value' => $data['to'],\n ],\n ];\n\n $timeFiltersWithoutEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ];\n\n $payload['filterGroups'] = [];\n\n $associationTypes = ['contact', 'company'];\n foreach ($associationTypes as $type) {\n if (! empty($data[$type])) {\n $filterGroupWithEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithEndTime;\n\n $filterGroupWithoutEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithoutEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithoutEndTime ;\n }\n }\n\n return $payload;\n }\n\n /** Parameters\n * [\n * 'accountId' => $crmAccountId,\n * 'sortBy' => $sortBy,\n * 'sortDir' => $sortDir,\n * 'onlyOpen' => $onlyOpen,\n * 'closedStages' => $this->getClosedDealStages(),\n * 'userId' => $userId,\n * ];\n */\n public function generateOpportunitiesSearchPayload(\n Configuration $config,\n string $crmAccountId,\n array $closedStages,\n ): array {\n $closedFilters = [];\n $filterGroups = [];\n $onlyOpen = true;\n\n switch ($config->getOpportunityAssignmentRule()) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'ASCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n default:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n $onlyOpen = false;\n }\n\n $baseFilters = [\n [\n 'propertyName' => 'associations.company',\n 'operator' => 'EQ',\n 'value' => $crmAccountId,\n ],\n ];\n\n // Handle closed stages in chunks\n if ($onlyOpen) {\n foreach (['won', 'lost'] as $key) {\n if (! empty($closedStages[$key])) {\n $chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);\n foreach ($chunks as $chunk) {\n $closedFilters[] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $chunk,\n ];\n }\n }\n }\n }\n\n $filterGroups[] = [\n 'filters' => array_merge($baseFilters, $closedFilters),\n ];\n\n $payload = [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => $sortBy,\n 'direction' => $sortDir,\n ],\n ],\n 'properties' => [\n 'dealname',\n 'amount',\n 'hubspot_owner_id',\n 'pipeline',\n 'dealstage',\n 'closedate',\n 'deal_currency_code',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n $logger = app(LoggerInterface::class);\n $logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * Converts v1 payload data to v3\n */\n public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array\n {\n // Use this mapping until the whole Hubspot V1 is dropped.\n $properties = [\n 'hubspot_owner_id' => $engagement['ownerId'],\n 'hs_timestamp' => $engagement['timestamp'],\n ];\n\n // $engagement['activityType'] is $activity->category->name\n if (isset($engagement['activityType'])) {\n $properties['hs_activity_type'] = $engagement['activityType'];\n }\n\n $metadataKeyMap = [\n 'hs_meeting_outcome' => 'meetingOutcome',\n 'hs_meeting_title' => 'title',\n 'hs_meeting_start_time' => 'startTime',\n 'hs_meeting_end_time' => 'endTime',\n 'hs_meeting_body' => 'body',\n 'hs_internal_meeting_notes' => 'internalMeetingNotes',\n ];\n\n foreach ($metadataKeyMap as $newKey => $oldKey) {\n if (isset($metadata[$oldKey])) {\n $properties[$newKey] = $metadata[$oldKey];\n unset($metadata[$oldKey]);\n }\n }\n\n $properties = [\n ...$properties,\n ...$metadata, // custom fields\n ];\n\n $response = [\n 'properties' => $properties,\n ];\n\n if (! empty($associations)) {\n $response['associations'] = $associations;\n }\n\n return $response;\n }\n\n /**\n * Generate a payload to search for contacts by name.\n * The search is a token search in firstname or lastname\n */\n public function generateSearchContactsByNamePayload(string $name, array $fields): array\n {\n $firstNameFilter = [\n 'propertyName' => 'firstname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n $lastNameFilter = [\n 'propertyName' => 'lastname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n return [\n 'filterGroups' => [\n [\n 'filters' => [$firstNameFilter],\n ],\n [\n 'filters' => [$lastNameFilter],\n ],\n ],\n 'properties' => $fields,\n 'sorts' => [\n [\n 'propertyName' => 'lastmodifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n ];\n }\n\n public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array\n {\n $inputs = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $inputs[] = [\n 'from' => [\n 'id' => $crmId,\n ],\n 'to' => [\n 'id' => (string) $id,\n ],\n 'types' => [\n [\n 'associationCategory' => 'HUBSPOT_DEFINED',\n 'associationTypeId' => $associationType,\n ],\n ],\n ];\n }\n\n return ['inputs' => $inputs];\n }\n\n public function buildRemoveAssociationPayload(string $crmId, array $ids): array\n {\n $toArray = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $toArray[] = ['id' => (string) $id];\n }\n\n return [\n 'inputs' => [\n [\n 'from' => ['id' => $crmId],\n 'to' => $toArray,\n ],\n ],\n ];\n }\n\n public function addClosedStageFilters(array &$payload, array $closedStages): void\n {\n if (! empty($closedStages['won'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['won'],\n ];\n }\n\n if (! empty($closedStages['lost'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['lost'],\n ];\n }\n }\n\n public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void\n {\n $payload['filters'][] = [\n 'propertyName' => 'createdate',\n 'operator' => 'GT',\n 'value' => $createdAfter->getPreciseTimestamp(3),\n ];\n }\n\n public function getDealsInBulkPayload(array $dealIds): array\n {\n return [\n 'filterGroups' => [\n [\n 'filters' => [\n [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'IN',\n 'values' => $dealIds,\n ],\n ],\n ],\n ],\n 'limit' => self::MAX_FILTER_SIZE,\n ];\n }\n}","depth":4,"bounds":{"left":0.122340426,"top":0.070231445,"width":0.35738033,"height":0.92976856},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Jiminny\\Models\\Crm\\Configuration;\nuse Psr\\Log\\LoggerInterface;\n\nclass PayloadBuilder\n{\n public const int MAX_SEARCH_REQUEST_LIMIT = 200;\n\n private const int MAX_FILTER_SIZE = 100;\n\n private const string SORT_PROPERTY = 'hs_timestamp';\n private const string ENGAGEMENT_MEETINGS = 'meetings';\n\n public function getLinkToTaskPayload(string $objectType, string $objectId, string $engagementType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($engagementType === self::ENGAGEMENT_MEETINGS) {\n $payload['filterGroups'] = $this->buildMeetingFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_timestamp',\n 'hs_activity_type',\n ];\n } else {\n $payload['filterGroups'] = $this->buildTaskFiltersForLinkToTask($objectType, $objectId);\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp'];\n }\n\n return $payload;\n }\n\n public function getRecentlyUpdatedSearchPayload(Carbon $since, ?Carbon $to, array $properties): array\n {\n $lastUpdateDate = in_array('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n public function getByProfileSearchPayload(string $crmId, array $properties, Carbon $since, ?Carbon $to): array\n {\n $lastUpdateDate = array_key_exists('firstname', $properties) ? 'lastmodifieddate' : 'hs_lastmodifieddate';\n\n $payload = [\n 'filters' => [\n [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'GT',\n 'value' => $since->getPreciseTimestamp(3),\n ],\n [\n 'propertyName' => 'hubspot_owner_id',\n 'operator' => 'EQ',\n 'value' => $crmId,\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_object_id',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($to) {\n $payload['filters'][] = [\n 'propertyName' => $lastUpdateDate,\n 'operator' => 'LT',\n 'value' => $to->getPreciseTimestamp(3),\n ];\n }\n\n $payload['properties'] = $properties;\n\n return $payload;\n }\n\n private function buildMeetingFiltersForLinkToTask($objectType, $objectId): array\n {\n $filters = [];\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'IN',\n 'values' => ['SCHEDULED', 'RESCHEDULED'],\n ],\n ],\n ];\n\n $filters[] = [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n [\n 'propertyName' => 'hs_meeting_outcome',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ],\n ];\n\n return $filters;\n }\n\n private function buildTaskFiltersForLinkToTask($objectType, $objectId): array\n {\n return [\n [\n 'filters' => [\n $this->getAssociatedObjectFilter($objectType, $objectId),\n ],\n ],\n ];\n }\n\n private function getAssociatedObjectFilter(string $objectType, string $objectId): array\n {\n return [\n 'propertyName' => 'associations.' . $objectType,\n 'operator' => 'EQ',\n 'value' => $objectId,\n ];\n }\n\n public function generatePlaybackURLSearchPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n public function generateGetCallsPayload(Carbon $from, Carbon $to, string $activityProvider, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $activityProvider,\n ],\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function generateSearchCallsByPeriodPayload(Carbon $from, Carbon $to, $page): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'operator' => 'BETWEEN',\n 'value' => intval($from->getPreciseTimestamp(3)),\n 'highValue' => intval($to->getPreciseTimestamp(3)),\n ],\n ],\n 'sorts' => [\n [\n 'propertyName' => 'hs_timestamp',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $this->getSearchCallAttributes(),\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n 'after' => ($page - 1) * self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n }\n\n public function getSearchCallAttributes(): array\n {\n return [\n 'hs_timestamp',\n 'hs_call_recording_url',\n 'hs_call_body',\n 'hs_call_status',\n 'hs_call_to_number',\n 'hs_call_from_number',\n 'hs_call_duration',\n 'hs_call_disposition',\n 'hs_call_title',\n 'hs_call_direction',\n 'hubspot_owner_id',\n 'hs_activity_type',\n 'hs_call_external_id',\n 'hs_call_source',\n ];\n }\n\n public function generatePlaybackAddUrlBatchPayload(array $crmUpdateData): array\n {\n $updateObjectsData = [];\n\n foreach ($crmUpdateData as $data) {\n $updateObjectsData[] = [\n 'id' => $data['crm_id'],\n 'properties' => [\n 'hs_call_body' => $data['hs_call_body'] .\n '<p><span style=\"font-size: 13.3333px; line-height: 16px;\">' .\n '<a href=\"' . $data['playback_url'] . '\" target=\"_blank\">Review in Jiminny</a> ▶️</span></p>',\n ],\n ];\n }\n\n return ['inputs' => $updateObjectsData];\n }\n\n public function generateSearchCallByTokenPayload(string $playbackURLToken): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => 'hs_call_recording_url',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $playbackURLToken,\n ],\n ],\n 'properties' => [\n 'hs_call_recording_url',\n 'hs_call_body',\n ],\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload for phone search based on the specified parameters.\n *\n * @param string $phone The phone number to search.\n * @param bool $isAlternativeSearch Indicates if an alternative search should be performed\n * if the first search fails to find a match. The first search request should cover most of the cases.\n */\n public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array\n {\n $filterPropertyNames = $isAlternativeSearch ?\n ['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :\n ['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];\n\n $filterGroups = array_map(function ($propertyName) use ($phone) {\n return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);\n }, $filterPropertyNames);\n\n $properties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n\n return $this->createPayload($filterGroups, $properties);\n }\n\n /**\n * Creates a filter group with the specified parameters.\n */\n private function createFilterGroup(string $propertyName, string $operator, $value): array\n {\n return [\n 'filters' => [\n [\n 'propertyName' => $propertyName,\n 'operator' => $operator,\n 'value' => $value,\n ],\n ],\n ];\n }\n\n /**\n * Creates a payload with the specified filter groups and properties.\n */\n private function createPayload(array $filterGroups, array $properties): array\n {\n return [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => $properties,\n 'limit' => 1,\n ];\n }\n\n /**\n * Generates a payload to find related activities based on the specified data and object type.\n * The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)\n * and are associated with any of the provided prospect types (contact or company).\n *\n * @param array $data An associative array containing the following keys:\n * - 'from': The start time for the activity search (timestamp).\n * - 'to': The end time for the activity search (timestamp).\n * - 'contact': (optional) The ID of the associated contact.\n * - 'company': (optional) The ID of the associated company.\n * @param string $objectType The type of engagement to search for (meeting or other type like call).\n *\n * @return array The payload array to be used for searching related activities.\n */\n public function getFindRelatedActivityPayload(array $data, string $objectType): array\n {\n $payload = [\n 'sorts' => [\n [\n 'propertyName' => self::SORT_PROPERTY,\n 'direction' => 'DESCENDING',\n ],\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n if ($objectType === self::ENGAGEMENT_MEETINGS) {\n $payload['properties'] = [\n 'hs_meeting_title',\n 'hs_meeting_outcome',\n 'hs_activity_type',\n 'hs_timestamp',\n 'hubspot_owner_id',\n 'hs_meeting_body',\n 'hs_internal_meeting_notes',\n 'hs_meeting_location',\n 'hs_meeting_start_time',\n 'hs_meeting_end_time',\n ];\n } else {\n $payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];\n }\n\n $timeFiltersWithEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'LTE',\n 'value' => $data['to'],\n ],\n ];\n\n $timeFiltersWithoutEndTime = [\n [\n 'propertyName' => 'hs_meeting_start_time',\n 'operator' => 'GTE',\n 'value' => $data['from'],\n ],\n [\n 'propertyName' => 'hs_meeting_end_time',\n 'operator' => 'NOT_HAS_PROPERTY',\n ],\n ];\n\n $payload['filterGroups'] = [];\n\n $associationTypes = ['contact', 'company'];\n foreach ($associationTypes as $type) {\n if (! empty($data[$type])) {\n $filterGroupWithEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithEndTime;\n\n $filterGroupWithoutEndTime = [\n 'filters' => array_merge(\n $timeFiltersWithoutEndTime,\n [$this->getAssociatedObjectFilter($type, $data[$type])]\n ),\n ];\n $payload['filterGroups'][] = $filterGroupWithoutEndTime ;\n }\n }\n\n return $payload;\n }\n\n /** Parameters\n * [\n * 'accountId' => $crmAccountId,\n * 'sortBy' => $sortBy,\n * 'sortDir' => $sortDir,\n * 'onlyOpen' => $onlyOpen,\n * 'closedStages' => $this->getClosedDealStages(),\n * 'userId' => $userId,\n * ];\n */\n public function generateOpportunitiesSearchPayload(\n Configuration $config,\n string $crmAccountId,\n array $closedStages,\n ): array {\n $closedFilters = [];\n $filterGroups = [];\n $onlyOpen = true;\n\n switch ($config->getOpportunityAssignmentRule()) {\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'DESCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:\n $sortBy = 'createdate';\n $sortDir = 'ASCENDING';\n\n break;\n\n case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:\n default:\n $sortBy = 'modifieddate';\n $sortDir = 'DESCENDING';\n $onlyOpen = false;\n }\n\n $baseFilters = [\n [\n 'propertyName' => 'associations.company',\n 'operator' => 'EQ',\n 'value' => $crmAccountId,\n ],\n ];\n\n // Handle closed stages in chunks\n if ($onlyOpen) {\n foreach (['won', 'lost'] as $key) {\n if (! empty($closedStages[$key])) {\n $chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);\n foreach ($chunks as $chunk) {\n $closedFilters[] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $chunk,\n ];\n }\n }\n }\n }\n\n $filterGroups[] = [\n 'filters' => array_merge($baseFilters, $closedFilters),\n ];\n\n $payload = [\n 'filterGroups' => $filterGroups,\n 'sorts' => [\n [\n 'propertyName' => $sortBy,\n 'direction' => $sortDir,\n ],\n ],\n 'properties' => [\n 'dealname',\n 'amount',\n 'hubspot_owner_id',\n 'pipeline',\n 'dealstage',\n 'closedate',\n 'deal_currency_code',\n ],\n 'limit' => self::MAX_SEARCH_REQUEST_LIMIT,\n ];\n\n $logger = app(LoggerInterface::class);\n $logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * Converts v1 payload data to v3\n */\n public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array\n {\n // Use this mapping until the whole Hubspot V1 is dropped.\n $properties = [\n 'hubspot_owner_id' => $engagement['ownerId'],\n 'hs_timestamp' => $engagement['timestamp'],\n ];\n\n // $engagement['activityType'] is $activity->category->name\n if (isset($engagement['activityType'])) {\n $properties['hs_activity_type'] = $engagement['activityType'];\n }\n\n $metadataKeyMap = [\n 'hs_meeting_outcome' => 'meetingOutcome',\n 'hs_meeting_title' => 'title',\n 'hs_meeting_start_time' => 'startTime',\n 'hs_meeting_end_time' => 'endTime',\n 'hs_meeting_body' => 'body',\n 'hs_internal_meeting_notes' => 'internalMeetingNotes',\n ];\n\n foreach ($metadataKeyMap as $newKey => $oldKey) {\n if (isset($metadata[$oldKey])) {\n $properties[$newKey] = $metadata[$oldKey];\n unset($metadata[$oldKey]);\n }\n }\n\n $properties = [\n ...$properties,\n ...$metadata, // custom fields\n ];\n\n $response = [\n 'properties' => $properties,\n ];\n\n if (! empty($associations)) {\n $response['associations'] = $associations;\n }\n\n return $response;\n }\n\n /**\n * Generate a payload to search for contacts by name.\n * The search is a token search in firstname or lastname\n */\n public function generateSearchContactsByNamePayload(string $name, array $fields): array\n {\n $firstNameFilter = [\n 'propertyName' => 'firstname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n $lastNameFilter = [\n 'propertyName' => 'lastname',\n 'operator' => 'CONTAINS_TOKEN',\n 'value' => $name,\n ];\n\n return [\n 'filterGroups' => [\n [\n 'filters' => [$firstNameFilter],\n ],\n [\n 'filters' => [$lastNameFilter],\n ],\n ],\n 'properties' => $fields,\n 'sorts' => [\n [\n 'propertyName' => 'lastmodifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n ];\n }\n\n public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array\n {\n $inputs = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $inputs[] = [\n 'from' => [\n 'id' => $crmId,\n ],\n 'to' => [\n 'id' => (string) $id,\n ],\n 'types' => [\n [\n 'associationCategory' => 'HUBSPOT_DEFINED',\n 'associationTypeId' => $associationType,\n ],\n ],\n ];\n }\n\n return ['inputs' => $inputs];\n }\n\n public function buildRemoveAssociationPayload(string $crmId, array $ids): array\n {\n $toArray = [];\n foreach ($ids as $id) {\n if ($id === null || $id === '') {\n continue;\n }\n\n $toArray[] = ['id' => (string) $id];\n }\n\n return [\n 'inputs' => [\n [\n 'from' => ['id' => $crmId],\n 'to' => $toArray,\n ],\n ],\n ];\n }\n\n public function addClosedStageFilters(array &$payload, array $closedStages): void\n {\n if (! empty($closedStages['won'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['won'],\n ];\n }\n\n if (! empty($closedStages['lost'])) {\n $payload['filters'][] = [\n 'propertyName' => 'dealstage',\n 'operator' => 'NOT_IN',\n 'values' => $closedStages['lost'],\n ];\n }\n }\n\n public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void\n {\n $payload['filters'][] = [\n 'propertyName' => 'createdate',\n 'operator' => 'GT',\n 'value' => $createdAfter->getPreciseTimestamp(3),\n ];\n }\n\n public function getDealsInBulkPayload(array $dealIds): array\n {\n return [\n 'filterGroups' => [\n [\n 'filters' => [\n [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'IN',\n 'values' => $dealIds,\n ],\n ],\n ],\n ],\n 'limit' => self::MAX_FILTER_SIZE,\n ];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-3653358008198671637
|
2079491755365055053
|
idle
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] string $phone The phone number to search.
* @param bool $isAlternativeSearch Indicates if an alternative search should be performed
* if the first search fails to find a match. The first search request should cover most of the cases.
*/
public function generatePhoneSearchPayload(string $phone, bool $isAlternativeSearch = false): array
{
$filterPropertyNames = $isAlternativeSearch ?
['hs_searchable_calculated_mobile_number', 'phone', 'mobilephone'] :
['hs_searchable_calculated_phone_number', 'hs_calculated_phone_number', 'hs_calculated_mobile_number'];
$filterGroups = array_map(function ($propertyName) use ($phone) {
return $this->createFilterGroup($propertyName, 'CONTAINS_TOKEN', $phone);
}, $filterPropertyNames);
$properties = [
'email',
'firstname',
'lastname',
'country',
'phone',
'mobilephone',
'jobtitle',
'hubspot_owner_id',
'associatedcompanyid',
'photo',
];
return $this->createPayload($filterGroups, $properties);
}
/**
* Creates a filter group with the specified parameters.
*/
private function createFilterGroup(string $propertyName, string $operator, $value): array
{
return [
'filters' => [
[
'propertyName' => $propertyName,
'operator' => $operator,
'value' => $value,
],
],
];
}
/**
* Creates a payload with the specified filter groups and properties.
*/
private function createPayload(array $filterGroups, array $properties): array
{
return [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => 'modifieddate',
'direction' => 'DESCENDING',
],
],
'properties' => $properties,
'limit' => 1,
];
}
/**
* Generates a payload to find related activities based on the specified data and object type.
* The search looks for activities that match the specified time range (starttime - endtime or only starttime with endtime not set)
* and are associated with any of the provided prospect types (contact or company).
*
* @param array $data An associative array containing the following keys:
* - 'from': The start time for the activity search (timestamp).
* - 'to': The end time for the activity search (timestamp).
* - 'contact': (optional) The ID of the associated contact.
* - 'company': (optional) The ID of the associated company.
* @param string $objectType The type of engagement to search for (meeting or other type like call).
*
* @return array The payload array to be used for searching related activities.
*/
public function getFindRelatedActivityPayload(array $data, string $objectType): array
{
$payload = [
'sorts' => [
[
'propertyName' => self::SORT_PROPERTY,
'direction' => 'DESCENDING',
],
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
if ($objectType === self::ENGAGEMENT_MEETINGS) {
$payload['properties'] = [
'hs_meeting_title',
'hs_meeting_outcome',
'hs_activity_type',
'hs_timestamp',
'hubspot_owner_id',
'hs_meeting_body',
'hs_internal_meeting_notes',
'hs_meeting_location',
'hs_meeting_start_time',
'hs_meeting_end_time',
];
} else {
$payload['properties'] = ['hs_task_subject', 'hs_timestamp', 'hubspot_owner_id', 'hs_call_body'];
}
$timeFiltersWithEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'LTE',
'value' => $data['to'],
],
];
$timeFiltersWithoutEndTime = [
[
'propertyName' => 'hs_meeting_start_time',
'operator' => 'GTE',
'value' => $data['from'],
],
[
'propertyName' => 'hs_meeting_end_time',
'operator' => 'NOT_HAS_PROPERTY',
],
];
$payload['filterGroups'] = [];
$associationTypes = ['contact', 'company'];
foreach ($associationTypes as $type) {
if (! empty($data[$type])) {
$filterGroupWithEndTime = [
'filters' => array_merge(
$timeFiltersWithEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithEndTime;
$filterGroupWithoutEndTime = [
'filters' => array_merge(
$timeFiltersWithoutEndTime,
[$this->getAssociatedObjectFilter($type, $data[$type])]
),
];
$payload['filterGroups'][] = $filterGroupWithoutEndTime ;
}
}
return $payload;
}
/** Parameters
* [
* 'accountId' => $crmAccountId,
* 'sortBy' => $sortBy,
* 'sortDir' => $sortDir,
* 'onlyOpen' => $onlyOpen,
* 'closedStages' => $this->getClosedDealStages(),
* 'userId' => $userId,
* ];
*/
public function generateOpportunitiesSearchPayload(
Configuration $config,
string $crmAccountId,
array $closedStages,
): array {
$closedFilters = [];
$filterGroups = [];
$onlyOpen = true;
switch ($config->getOpportunityAssignmentRule()) {
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_UPDATED:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_RECENTLY_CREATED:
$sortBy = 'createdate';
$sortDir = 'DESCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_OPEN_OLDEST_CREATED:
$sortBy = 'createdate';
$sortDir = 'ASCENDING';
break;
case Configuration::OPP_ASSIGNMENT_ALL_RECENTLY_UPDATED:
default:
$sortBy = 'modifieddate';
$sortDir = 'DESCENDING';
$onlyOpen = false;
}
$baseFilters = [
[
'propertyName' => 'associations.company',
'operator' => 'EQ',
'value' => $crmAccountId,
],
];
// Handle closed stages in chunks
if ($onlyOpen) {
foreach (['won', 'lost'] as $key) {
if (! empty($closedStages[$key])) {
$chunks = array_chunk($closedStages[$key], self::MAX_FILTER_SIZE);
foreach ($chunks as $chunk) {
$closedFilters[] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $chunk,
];
}
}
}
}
$filterGroups[] = [
'filters' => array_merge($baseFilters, $closedFilters),
];
$payload = [
'filterGroups' => $filterGroups,
'sorts' => [
[
'propertyName' => $sortBy,
'direction' => $sortDir,
],
],
'properties' => [
'dealname',
'amount',
'hubspot_owner_id',
'pipeline',
'dealstage',
'closedate',
'deal_currency_code',
],
'limit' => self::MAX_SEARCH_REQUEST_LIMIT,
];
$logger = app(LoggerInterface::class);
$logger->info('[HubSpot] CRM Search requested', [
'request' => $payload,
]);
return $payload;
}
/**
* Converts v1 payload data to v3
*/
public function getV3MeetingPayload(array $engagement, array $metadata, array $associations = []): array
{
// Use this mapping until the whole Hubspot V1 is dropped.
$properties = [
'hubspot_owner_id' => $engagement['ownerId'],
'hs_timestamp' => $engagement['timestamp'],
];
// $engagement['activityType'] is $activity->category->name
if (isset($engagement['activityType'])) {
$properties['hs_activity_type'] = $engagement['activityType'];
}
$metadataKeyMap = [
'hs_meeting_outcome' => 'meetingOutcome',
'hs_meeting_title' => 'title',
'hs_meeting_start_time' => 'startTime',
'hs_meeting_end_time' => 'endTime',
'hs_meeting_body' => 'body',
'hs_internal_meeting_notes' => 'internalMeetingNotes',
];
foreach ($metadataKeyMap as $newKey => $oldKey) {
if (isset($metadata[$oldKey])) {
$properties[$newKey] = $metadata[$oldKey];
unset($metadata[$oldKey]);
}
}
$properties = [
...$properties,
...$metadata, // custom fields
];
$response = [
'properties' => $properties,
];
if (! empty($associations)) {
$response['associations'] = $associations;
}
return $response;
}
/**
* Generate a payload to search for contacts by name.
* The search is a token search in firstname or lastname
*/
public function generateSearchContactsByNamePayload(string $name, array $fields): array
{
$firstNameFilter = [
'propertyName' => 'firstname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
$lastNameFilter = [
'propertyName' => 'lastname',
'operator' => 'CONTAINS_TOKEN',
'value' => $name,
];
return [
'filterGroups' => [
[
'filters' => [$firstNameFilter],
],
[
'filters' => [$lastNameFilter],
],
],
'properties' => $fields,
'sorts' => [
[
'propertyName' => 'lastmodifieddate',
'direction' => 'DESCENDING',
],
],
];
}
public function buildAddAssociationPayload(string $crmId, array $ids, int $associationType): array
{
$inputs = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$inputs[] = [
'from' => [
'id' => $crmId,
],
'to' => [
'id' => (string) $id,
],
'types' => [
[
'associationCategory' => 'HUBSPOT_DEFINED',
'associationTypeId' => $associationType,
],
],
];
}
return ['inputs' => $inputs];
}
public function buildRemoveAssociationPayload(string $crmId, array $ids): array
{
$toArray = [];
foreach ($ids as $id) {
if ($id === null || $id === '') {
continue;
}
$toArray[] = ['id' => (string) $id];
}
return [
'inputs' => [
[
'from' => ['id' => $crmId],
'to' => $toArray,
],
],
];
}
public function addClosedStageFilters(array &$payload, array $closedStages): void
{
if (! empty($closedStages['won'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['won'],
];
}
if (! empty($closedStages['lost'])) {
$payload['filters'][] = [
'propertyName' => 'dealstage',
'operator' => 'NOT_IN',
'values' => $closedStages['lost'],
];
}
}
public function addCreatedDateFilters(array &$payload, Carbon $createdAfter): void
{
$payload['filters'][] = [
'propertyName' => 'createdate',
'operator' => 'GT',
'value' => $createdAfter->getPreciseTimestamp(3),
];
}
public function getDealsInBulkPayload(array $dealIds): array
{
return [
'filterGroups' => [
[
'filters' => [
[
'propertyName' => 'hs_object_id',
'operator' => 'IN',
'values' => $dealIds,
],
],
],
],
'limit' => self::MAX_FILTER_SIZE,
];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
4833
|
NULL
|
NULL
|
NULL
|
|
4836
|
173
|
40
|
2026-05-07T14:32:04.187218+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164324187_m1.jpg...
|
PhpStorm
|
faVsco.js – RateLimitException.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
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;
Code changed:
Hide
Sync Changes
Hide This Notification...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8207425349720185171
|
921758650370266701
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
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;
Code changed:
Hide
Sync Changes
Hide This Notification...
|
4830
|
NULL
|
NULL
|
NULL
|
|
4837
|
174
|
30
|
2026-05-07T14:32:04.187216+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164324187_m2.jpg...
|
PhpStorm
|
faVsco.js – RateLimitException.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
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;
Code changed:
Hide
Sync Changes
Hide This Notification
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends LogicException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.39660904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.40525267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4162234,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.42486703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.43351063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.44448137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4554521,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.4820479,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.49301863,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.6821808,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.66855055,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.67852396,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6878325,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6974734,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.70478725,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends LogicException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","depth":4,"bounds":{"left":0.11934841,"top":0.24581006,"width":0.27659574,"height":0.73343974},"on_screen":true,"lines":[{"char_start":0,"char_count":6,"bounds":{"left":0.11934841,"top":0.0,"width":0.012965426,"height":0.014365523}},{"char_start":7,"char_count":25,"bounds":{"left":0.11934841,"top":0.0,"width":0.06216755,"height":0.014365523}},{"char_start":33,"char_count":30,"bounds":{"left":0.11934841,"top":0.03431764,"width":0.07513298,"height":0.014365523}},{"char_start":64,"char_count":15,"bounds":{"left":0.11934841,"top":0.06943336,"width":0.036236703,"height":0.014365523}},{"char_start":80,"char_count":48,"bounds":{"left":0.11934841,"top":0.10454908,"width":0.12167553,"height":0.014365523}},{"char_start":128,"char_count":2,"bounds":{"left":0.11934841,"top":0.12210695,"width":0.0026595744,"height":0.014365523}},{"char_start":130,"char_count":33,"bounds":{"left":0.11934841,"top":0.1396648,"width":0.08277926,"height":0.014365523}},{"char_start":163,"char_count":30,"bounds":{"left":0.11934841,"top":0.15722266,"width":0.07513298,"height":0.014365523}},{"char_start":193,"char_count":46,"bounds":{"left":0.11934841,"top":0.17478053,"width":0.11635638,"height":0.014365523}},{"char_start":239,"char_count":37,"bounds":{"left":0.11934841,"top":0.19233839,"width":0.0930851,"height":0.014365523}},{"char_start":276,"char_count":8,"bounds":{"left":0.11934841,"top":0.20989625,"width":0.017952127,"height":0.014365523}},{"char_start":284,"char_count":53,"bounds":{"left":0.11934841,"top":0.22745411,"width":0.14993352,"height":0.014365523}},{"char_start":337,"char_count":6,"bounds":{"left":0.11934841,"top":0.24501197,"width":0.012965426,"height":0.014365523}},{"char_start":344,"char_count":41,"bounds":{"left":0.11934841,"top":0.29768556,"width":0.10372341,"height":0.014365523}},{"char_start":385,"char_count":6,"bounds":{"left":0.11934841,"top":0.31524342,"width":0.012965426,"height":0.014365523}},{"char_start":391,"char_count":42,"bounds":{"left":0.11934841,"top":0.33280128,"width":0.12732713,"height":0.014365523}},{"char_start":433,"char_count":6,"bounds":{"left":0.11934841,"top":0.35035914,"width":0.012965426,"height":0.014365523}},{"char_start":439,"char_count":1,"bounds":{"left":0.11934841,"top":0.367917,"width":0.0026595744,"height":0.014365523}}],"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Exceptions;\n\nuse Throwable;\n\nclass RateLimitException extends LogicException\n{\n public function __construct(\n string $message = '',\n private readonly int $retryAfter = 1,\n ?Throwable $previous = null,\n ) {\n parent::__construct($message, 0, $previous);\n }\n\n public function getRetryAfter(): int\n {\n return max($this->retryAfter, 1);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
3043929094077580694
|
2074750523721291341
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
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;
Code changed:
Hide
Sync Changes
Hide This Notification
<?php
declare(strict_types=1);
namespace Jiminny\Exceptions;
use Throwable;
class RateLimitException extends LogicException
{
public function __construct(
string $message = '',
private readonly int $retryAfter = 1,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function getRetryAfter(): int
{
return max($this->retryAfter, 1);
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
4833
|
NULL
|
NULL
|
NULL
|
|
4838
|
174
|
31
|
2026-05-07T14:32:08.437507+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164328437_m2.jpg...
|
PhpStorm
|
faVsco.js – DeleteCrmEntityTrait.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
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
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Crm\Delete;
use DateTimeInterface;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Log;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Crm\DetachActivityObject;
use Jiminny\Jobs\Middleware\HandleRateLimit;
use Jiminny\Models\Activity;
use Psr\Log\LoggerInterface;
use Throwable;
trait DeleteCrmEntityTrait
{
public int $tries = 3;
public function timeout(): int
{
return 300; // 5 minutes
}
public function backoff(): array
{
return [30, 90, 180]; // 30 seconds, 1.5 minutes, 3 minutes
}
/**
* Wall-clock deadline so rate-limit releases (which still increment
* $job->attempts()) don't kill the job during a long backlog drain.
* Real failures are still bounded by $tries + backoff().
*/
public function retryUntil(): DateTimeInterface
{
return Date::now()->addHour();
}
public function middleware(): array
{
return [new HandleRateLimit()];
}
protected function handleActivities(
Collection $activities,
Dispatcher $dispatcher,
LoggerInterface $logger,
bool $emitEvent = true,
): void {
if ($activities->isEmpty()) {
return;
}
$crmObject = $this->getEntityType();
$entityIdField = $crmObject->value . '_id';
$activities->each(
function (Activity $activity) use ($dispatcher, $logger, $entityIdField, $crmObject, $emitEvent): void {
$stageId = $activity->getStage()?->getId();
$logData = [
$crmObject->value => $this->id,
'activity' => $activity->getId(),
'emitEvent' => $emitEvent,
];
$updateData = [
$entityIdField => null,
];
// For leads and opportunities, also nullify the stage_id
if ($stageId && in_array($crmObject, [CrmObject::LEAD, CrmObject::OPPORTUNITY], true)) {
$updateData['stage_id'] = null;
$logData['stage_id'] = $stageId;
}
$activity->update($updateData);
if ($emitEvent) {
$dispatcher->dispatch(new DetachActivityObject($activity, $crmObject));
}
$logger->info($this->getLogPrefix() . ' Detach from activity', $logData);
// Dispatch job to verify if CRM task/event still exists
if ($activity->hasCrmProviderId()) {
VerifyActivityCrmTaskJob::dispatch($activity->getId());
}
}
);
}
public function failed(Throwable $exception): void
{
$crmObject = $this->getEntityType();
Log::critical($this->getLogPrefix() . ' Job failed permanently', [
$crmObject->value => $this->id,
'exception' => $exception->getMessage(),
]);
}
// Abstract methods that must be implemented by the using class
abstract protected function getLogPrefix(): string;
abstract protected function getEntityType(): CrmObject;
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.39660904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.40525267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4162234,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.42486703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.43351063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.44448137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4554521,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.4820479,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.49301863,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.6821808,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.66855055,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.67852396,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6878325,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6974734,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.70478725,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"2","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.007978723,"height":0.0},"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.006981383,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Crm\\Delete;\n\nuse DateTimeInterface;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Date;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Crm\\DetachActivityObject;\nuse Jiminny\\Jobs\\Middleware\\HandleRateLimit;\nuse Jiminny\\Models\\Activity;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\ntrait DeleteCrmEntityTrait\n{\n public int $tries = 3;\n\n public function timeout(): int\n {\n return 300; // 5 minutes\n }\n\n public function backoff(): array\n {\n return [30, 90, 180]; // 30 seconds, 1.5 minutes, 3 minutes\n }\n\n /**\n * Wall-clock deadline so rate-limit releases (which still increment\n * $job->attempts()) don't kill the job during a long backlog drain.\n * Real failures are still bounded by $tries + backoff().\n */\n public function retryUntil(): DateTimeInterface\n {\n return Date::now()->addHour();\n }\n\n public function middleware(): array\n {\n return [new HandleRateLimit()];\n }\n\n protected function handleActivities(\n Collection $activities,\n Dispatcher $dispatcher,\n LoggerInterface $logger,\n bool $emitEvent = true,\n ): void {\n if ($activities->isEmpty()) {\n return;\n }\n\n $crmObject = $this->getEntityType();\n $entityIdField = $crmObject->value . '_id';\n\n $activities->each(\n function (Activity $activity) use ($dispatcher, $logger, $entityIdField, $crmObject, $emitEvent): void {\n $stageId = $activity->getStage()?->getId();\n\n $logData = [\n $crmObject->value => $this->id,\n 'activity' => $activity->getId(),\n 'emitEvent' => $emitEvent,\n ];\n\n $updateData = [\n $entityIdField => null,\n ];\n\n // For leads and opportunities, also nullify the stage_id\n if ($stageId && in_array($crmObject, [CrmObject::LEAD, CrmObject::OPPORTUNITY], true)) {\n $updateData['stage_id'] = null;\n $logData['stage_id'] = $stageId;\n }\n\n $activity->update($updateData);\n\n if ($emitEvent) {\n $dispatcher->dispatch(new DetachActivityObject($activity, $crmObject));\n }\n\n $logger->info($this->getLogPrefix() . ' Detach from activity', $logData);\n\n // Dispatch job to verify if CRM task/event still exists\n if ($activity->hasCrmProviderId()) {\n VerifyActivityCrmTaskJob::dispatch($activity->getId());\n }\n }\n );\n }\n\n public function failed(Throwable $exception): void\n {\n $crmObject = $this->getEntityType();\n\n Log::critical($this->getLogPrefix() . ' Job failed permanently', [\n $crmObject->value => $this->id,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n // Abstract methods that must be implemented by the using class\n abstract protected function getLogPrefix(): string;\n abstract protected function getEntityType(): CrmObject;\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Jobs\\Crm\\Delete;\n\nuse DateTimeInterface;\nuse Illuminate\\Events\\Dispatcher;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Facades\\Date;\nuse Illuminate\\Support\\Facades\\Log;\nuse Jiminny\\Enums\\CrmObject;\nuse Jiminny\\Events\\Crm\\DetachActivityObject;\nuse Jiminny\\Jobs\\Middleware\\HandleRateLimit;\nuse Jiminny\\Models\\Activity;\nuse Psr\\Log\\LoggerInterface;\nuse Throwable;\n\ntrait DeleteCrmEntityTrait\n{\n public int $tries = 3;\n\n public function timeout(): int\n {\n return 300; // 5 minutes\n }\n\n public function backoff(): array\n {\n return [30, 90, 180]; // 30 seconds, 1.5 minutes, 3 minutes\n }\n\n /**\n * Wall-clock deadline so rate-limit releases (which still increment\n * $job->attempts()) don't kill the job during a long backlog drain.\n * Real failures are still bounded by $tries + backoff().\n */\n public function retryUntil(): DateTimeInterface\n {\n return Date::now()->addHour();\n }\n\n public function middleware(): array\n {\n return [new HandleRateLimit()];\n }\n\n protected function handleActivities(\n Collection $activities,\n Dispatcher $dispatcher,\n LoggerInterface $logger,\n bool $emitEvent = true,\n ): void {\n if ($activities->isEmpty()) {\n return;\n }\n\n $crmObject = $this->getEntityType();\n $entityIdField = $crmObject->value . '_id';\n\n $activities->each(\n function (Activity $activity) use ($dispatcher, $logger, $entityIdField, $crmObject, $emitEvent): void {\n $stageId = $activity->getStage()?->getId();\n\n $logData = [\n $crmObject->value => $this->id,\n 'activity' => $activity->getId(),\n 'emitEvent' => $emitEvent,\n ];\n\n $updateData = [\n $entityIdField => null,\n ];\n\n // For leads and opportunities, also nullify the stage_id\n if ($stageId && in_array($crmObject, [CrmObject::LEAD, CrmObject::OPPORTUNITY], true)) {\n $updateData['stage_id'] = null;\n $logData['stage_id'] = $stageId;\n }\n\n $activity->update($updateData);\n\n if ($emitEvent) {\n $dispatcher->dispatch(new DetachActivityObject($activity, $crmObject));\n }\n\n $logger->info($this->getLogPrefix() . ' Detach from activity', $logData);\n\n // Dispatch job to verify if CRM task/event still exists\n if ($activity->hasCrmProviderId()) {\n VerifyActivityCrmTaskJob::dispatch($activity->getId());\n }\n }\n );\n }\n\n public function failed(Throwable $exception): void\n {\n $crmObject = $this->getEntityType();\n\n Log::critical($this->getLogPrefix() . ' Job failed permanently', [\n $crmObject->value => $this->id,\n 'exception' => $exception->getMessage(),\n ]);\n }\n\n // Abstract methods that must be implemented by the using class\n abstract protected function getLogPrefix(): string;\n abstract protected function getEntityType(): CrmObject;\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-8463527390335742190
|
921830118623998541
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
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
2
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Jobs\Crm\Delete;
use DateTimeInterface;
use Illuminate\Events\Dispatcher;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Log;
use Jiminny\Enums\CrmObject;
use Jiminny\Events\Crm\DetachActivityObject;
use Jiminny\Jobs\Middleware\HandleRateLimit;
use Jiminny\Models\Activity;
use Psr\Log\LoggerInterface;
use Throwable;
trait DeleteCrmEntityTrait
{
public int $tries = 3;
public function timeout(): int
{
return 300; // 5 minutes
}
public function backoff(): array
{
return [30, 90, 180]; // 30 seconds, 1.5 minutes, 3 minutes
}
/**
* Wall-clock deadline so rate-limit releases (which still increment
* $job->attempts()) don't kill the job during a long backlog drain.
* Real failures are still bounded by $tries + backoff().
*/
public function retryUntil(): DateTimeInterface
{
return Date::now()->addHour();
}
public function middleware(): array
{
return [new HandleRateLimit()];
}
protected function handleActivities(
Collection $activities,
Dispatcher $dispatcher,
LoggerInterface $logger,
bool $emitEvent = true,
): void {
if ($activities->isEmpty()) {
return;
}
$crmObject = $this->getEntityType();
$entityIdField = $crmObject->value . '_id';
$activities->each(
function (Activity $activity) use ($dispatcher, $logger, $entityIdField, $crmObject, $emitEvent): void {
$stageId = $activity->getStage()?->getId();
$logData = [
$crmObject->value => $this->id,
'activity' => $activity->getId(),
'emitEvent' => $emitEvent,
];
$updateData = [
$entityIdField => null,
];
// For leads and opportunities, also nullify the stage_id
if ($stageId && in_array($crmObject, [CrmObject::LEAD, CrmObject::OPPORTUNITY], true)) {
$updateData['stage_id'] = null;
$logData['stage_id'] = $stageId;
}
$activity->update($updateData);
if ($emitEvent) {
$dispatcher->dispatch(new DetachActivityObject($activity, $crmObject));
}
$logger->info($this->getLogPrefix() . ' Detach from activity', $logData);
// Dispatch job to verify if CRM task/event still exists
if ($activity->hasCrmProviderId()) {
VerifyActivityCrmTaskJob::dispatch($activity->getId());
}
}
);
}
public function failed(Throwable $exception): void
{
$crmObject = $this->getEntityType();
Log::critical($this->getLogPrefix() . ' Job failed permanently', [
$crmObject->value => $this->id,
'exception' => $exception->getMessage(),
]);
}
// Abstract methods that must be implemented by the using class
abstract protected function getLogPrefix(): string;
abstract protected function getEntityType(): CrmObject;
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4839
|
173
|
41
|
2026-05-07T14:32:08.534791+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164328534_m1.jpg...
|
PhpStorm
|
faVsco.js – DeleteCrmEntityTrait.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-983685874644596884
|
-8165789139498234996
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.comLukas/Stefka 121 • now100% <28•Thu 7 May 17:32:08Lukas Kovalik5:32 PM | Lukas/Stefka 121• 1:30...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4840
|
NULL
|
0
|
2026-05-07T14:32:17.932297+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164337932_m1.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n// if ($this->shouldStopPagination($state, $teamId)) {\n// break;\n// }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n// $this->validateTokenIfNeeded($client, $state);\n// usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n// $state->setTotal($page['total'] ?? 0);\n// $this->updateLastRecordId($page, $state);\n//\n// // Safely iterate over results with null check\n// $results = $page['results'] ?? [];\n// foreach ($results as $row) {\n// $state->incrementTotalRecords();\n// yield $row;\n// }\n//\n// $state->setOffset($this->getNextOffset($page));\n// $state->incrementRequestCount();\n//\n// $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n\n yield;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $headers = $response->getHeaders();\n $this->logger->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else if ($client->isHubspotRateLimit($e)) {\n $retryAfter = $client->parseRetryAfter($e);\n\n $this->logger->info('[Hubspot] Received 429 from API', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'config_id' => $client->getConfig()->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n// if ($this->shouldStopPagination($state, $teamId)) {\n// break;\n// }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n// $this->validateTokenIfNeeded($client, $state);\n// usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n// $state->setTotal($page['total'] ?? 0);\n// $this->updateLastRecordId($page, $state);\n//\n// // Safely iterate over results with null check\n// $results = $page['results'] ?? [];\n// foreach ($results as $row) {\n// $state->incrementTotalRecords();\n// yield $row;\n// }\n//\n// $state->setOffset($this->getNextOffset($page));\n// $state->incrementRequestCount();\n//\n// $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n\n yield;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $headers = $response->getHeaders();\n $this->logger->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else if ($client->isHubspotRateLimit($e)) {\n $retryAfter = $client->parseRetryAfter($e);\n\n $this->logger->info('[Hubspot] Received 429 from API', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'config_id' => $client->getConfig()->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6444685682806833012
|
923237493509633613
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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...
|
4839
|
NULL
|
NULL
|
NULL
|
|
4841
|
NULL
|
0
|
2026-05-07T14:32:17.932427+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164337932_m2.jpg...
|
PhpStorm
|
faVsco.js – HubspotPaginationService.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.39660904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.40525267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4162234,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.42486703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.43351063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.44448137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4554521,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.4820479,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.49301863,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.6821808,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.66855055,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.67852396,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6878325,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6974734,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.70478725,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"19","depth":4,"bounds":{"left":0.3600399,"top":0.2490024,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37167552,"top":0.2490024,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.3806516,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.3879654,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n// if ($this->shouldStopPagination($state, $teamId)) {\n// break;\n// }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n// $this->validateTokenIfNeeded($client, $state);\n// usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n// $state->setTotal($page['total'] ?? 0);\n// $this->updateLastRecordId($page, $state);\n//\n// // Safely iterate over results with null check\n// $results = $page['results'] ?? [];\n// foreach ($results as $row) {\n// $state->incrementTotalRecords();\n// yield $row;\n// }\n//\n// $state->setOffset($this->getNextOffset($page));\n// $state->incrementRequestCount();\n//\n// $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n\n yield;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $headers = $response->getHeaders();\n $this->logger->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else if ($client->isHubspotRateLimit($e)) {\n $retryAfter = $client->parseRetryAfter($e);\n\n $this->logger->info('[Hubspot] Received 429 from API', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'config_id' => $client->getConfig()->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm\\Hubspot\\Pagination;\n\nuse Jiminny\\Exceptions\\RateLimitException;\nuse Jiminny\\Services\\Crm\\Hubspot\\Client;\nuse Jiminny\\Services\\Crm\\Hubspot\\PayloadBuilder;\nuse Psr\\Log\\LoggerInterface;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse SevenShores\\Hubspot\\Exceptions\\HubspotException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\n\nclass HubspotPaginationService\n{\n public function __construct(\n private LoggerInterface $logger\n ) {\n }\n\n /**\n * @throws HubspotException\n * @throws SocialAccountTokenInvalidException\n * @throws BadRequest\n */\n public function getPaginatedDataGenerator(\n Client $client,\n array $payload,\n string $type,\n int $offset = 0,\n int &$total = 0,\n ?string &$lastRecordId = null\n ): \\Generator {\n $state = new PaginationState(offset: $offset);\n $endpoint = Client::BASE_URL . \"/crm/v3/objects/{$type}/search\";\n $defaultFilter = $payload['filters'] ?? [];\n $resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;\n $teamId = $client->getConfig()->getTeam()->getId();\n $delay = $this->calculateDelayInMicroseconds();\n\n do {\n// if ($this->shouldStopPagination($state, $teamId)) {\n// break;\n// }\n\n $payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);\n\n// $this->validateTokenIfNeeded($client, $state);\n// usleep($delay);\n\n $page = $this->executeSearchRequest($client, $endpoint, $payload, $state);\n\n// $state->setTotal($page['total'] ?? 0);\n// $this->updateLastRecordId($page, $state);\n//\n// // Safely iterate over results with null check\n// $results = $page['results'] ?? [];\n// foreach ($results as $row) {\n// $state->incrementTotalRecords();\n// yield $row;\n// }\n//\n// $state->setOffset($this->getNextOffset($page));\n// $state->incrementRequestCount();\n//\n// $this->logPaginationProgress($state, $teamId, $endpoint);\n } while ($state->offset && ! empty($page['results']));\n\n // Log final pagination completion stats\n $this->logger->info('[Hubspot] Pagination completed', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'total_requests' => $state->requestCount,\n 'total_records_fetched' => $state->totalRecords,\n 'total_elapsed_seconds' => round($state->getElapsedSeconds(), 2),\n 'average_seconds_per_request' => $state->requestCount > 0 ? round($state->getElapsedSeconds() / $state->requestCount, 2) : 0,\n ]);\n\n // Update reference parameters\n $total = $state->total;\n $lastRecordId = $state->lastRecordId;\n\n yield;\n }\n\n private function shouldStopPagination(PaginationState $state, int $teamId): bool\n {\n if ($state->hasReachedSafetyLimit()) {\n $this->logger->warning('[Hubspot] Reached maximum request limit during pagination', [\n 'team_id' => $teamId,\n 'safety_limit' => PaginationConfig::LOOP_SAFETY_LIMIT,\n 'total_fetched' => $state->totalRecords,\n ]);\n\n return true;\n }\n\n return false;\n }\n\n private function handlePaginationStrategy(\n array $payload,\n array $defaultFilter,\n PaginationState $state,\n int $resultsPerPage,\n int $teamId\n ): array {\n if ($this->shouldSwitchToKeysetPagination($state, $resultsPerPage)) {\n $payload['filters'] = $defaultFilter;\n $payload['filters'][] = [\n 'propertyName' => 'hs_object_id',\n 'operator' => 'LT',\n 'value' => $state->lastRecordId,\n ];\n\n $this->logger->info('[Hubspot] Search keyset pagination request', [\n 'team_id' => $teamId,\n 'sequence' => $state->requestCount,\n 'itemsPerPage' => $resultsPerPage,\n 'payload' => $payload,\n 'total' => $state->total,\n ]);\n\n unset($payload['after']);\n $state->setOffset(0);\n }\n\n if ($state->offset) {\n $payload['after'] = $state->offset;\n }\n\n return $payload;\n }\n\n private function shouldSwitchToKeysetPagination(PaginationState $state, int $resultsPerPage): bool\n {\n // Check if we've hit the offset limit\n $shouldSwitch = $state->requestCount > 0 && ($state->offset + $resultsPerPage) > PaginationConfig::TOTAL_QUERY_LIMIT;\n\n if ($shouldSwitch && $state->lastRecordId === null) {\n $this->logger->warning('[Hubspot] Cannot switch to keyset pagination: lastRecordId is null', [\n 'request_count' => $state->requestCount,\n 'current_offset' => $state->offset,\n 'results_per_page' => $resultsPerPage,\n 'total_query_limit' => PaginationConfig::TOTAL_QUERY_LIMIT,\n ]);\n\n return false; // Continue with offset pagination\n }\n\n return $shouldSwitch;\n }\n\n private function validateTokenIfNeeded(Client $client, PaginationState $state): void\n {\n if ($state->shouldValidateToken()) {\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n }\n }\n\n private function executeSearchRequest(Client $client, string $endpoint, array $payload, PaginationState $state): array\n {\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $headers = $response->getHeaders();\n $this->logger->info('[Hubspot] DEBUG Getting headers', [\n 'headers' => $headers ?? [],\n ]);\n\n return $response->toArray();\n } catch (\\Exception $e) {\n if ($client->isUnauthorizedException($e)) {\n $this->logger->warning('[Hubspot] Got 401 during pagination, attempting token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'error' => $e->getMessage(),\n ]);\n\n $client->ensureValidToken();\n $state->updateLastTokenCheck();\n\n try {\n $response = $client->getInstance()->getClient()->request('POST', $endpoint, ['json' => $payload]);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $response->toArray();\n } catch (\\Exception $retryException) {\n $this->logger->error('[Hubspot] Retry request failed after token refresh', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'original_error' => $e->getMessage(),\n 'retry_error' => $retryException->getMessage(),\n ]);\n\n throw $retryException;\n }\n } else if ($client->isHubspotRateLimit($e)) {\n $retryAfter = $client->parseRetryAfter($e);\n\n $this->logger->info('[Hubspot] Received 429 from API', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n 'config_id' => $client->getConfig()->getId(),\n 'retry_after' => $retryAfter,\n 'reason' => $e->getMessage(),\n ]);\n\n throw new RateLimitException('Hubspot returned 429', $retryAfter, $e);\n } else {\n throw $e;\n }\n }\n }\n\n private function updateLastRecordId(array $page, PaginationState $state): void\n {\n $lastRecord = ! empty($page['results']) ? end($page['results']) : null;\n $lastRecordId = $lastRecord['id'] ?? null;\n $state->updateLastRecordId($lastRecordId);\n }\n\n private function getNextOffset(array $page): int\n {\n return isset($page['paging']['next']['after']) ? (int) $page['paging']['next']['after'] : 0;\n }\n\n private function logPaginationProgress(PaginationState $state, int $teamId, string $endpoint): void\n {\n if ($state->shouldLogProgress()) {\n $this->logger->info('[Hubspot] Pagination progress log', [\n 'team_id' => $teamId,\n 'endpoint' => $endpoint,\n 'requests_made' => $state->requestCount,\n 'records_fetched' => $state->totalRecords,\n 'elapsed_seconds' => $state->getElapsedSeconds(),\n ]);\n }\n }\n\n private function calculateDelayInMicroseconds(): int\n {\n return (int) (1 / PaginationConfig::SEARCH_RPS_LIMIT * 1000000);\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6444685682806833012
|
923237493509633613
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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...
|
4838
|
NULL
|
NULL
|
NULL
|
|
4842
|
175
|
0
|
2026-05-07T14:32:39.775654+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164359775_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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) {
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"49","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n return null;\n\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n return null;\n\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false}]...
|
-5081709979380413320
|
4074613745204205637
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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) {
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4843
|
176
|
0
|
2026-05-07T14:32:39.775659+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164359775_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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) {
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.39660904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.40525267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4162234,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.42486703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.43351063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.44448137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4554521,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.4820479,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.49301863,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.6821808,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.66855055,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.67852396,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6878325,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6974734,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.70478725,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.32779256,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"49","depth":4,"bounds":{"left":0.33776596,"top":0.2490024,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.35006648,"top":0.2490024,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"bounds":{"left":0.359375,"top":0.2490024,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37167552,"top":0.2490024,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.3806516,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.3879654,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n return null;\n\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n return null;\n\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5081709979380413320
|
4074613745204205637
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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) {
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4844
|
175
|
1
|
2026-05-07T14:32:46.332983+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164366332_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-109916406326757632
|
-8168392714279744692
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
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
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.comLukas/Stefka 121 • now100% <28•Thu 7 May 17:32:46Lukas Kovalik5:32 PM | Lukas/Stefka 121• 2:08...
|
4842
|
NULL
|
NULL
|
NULL
|
|
4845
|
175
|
2
|
2026-05-07T14:32:48.015545+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164368015_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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) {
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"49","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n return null;\n\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n return null;\n\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5081709979380413320
|
4074613745204205637
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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) {
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4846
|
175
|
3
|
2026-05-07T14:33:01.469434+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164381469_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-109916406326757632
|
-8168392714279744692
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
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
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp• = @ @ meet.google.com/axk-zwsm-vok?authuser=lukas.kovalik%40jiminny.comLukas/Stefka 121 • now100% <8•Thu 7 May 17:33:01Lukas Kovalik5:33 PM | Lukas/Stefka 121• 2:23...
|
4845
|
NULL
|
NULL
|
NULL
|
|
4847
|
175
|
4
|
2026-05-07T14:33:02.885287+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164382885_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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) {
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"49","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n return null;\n\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n return null;\n\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5081709979380413320
|
4074613745204205637
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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) {
...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
4848
|
176
|
1
|
2026-05-07T14:33:10.610740+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164390610_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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) {
...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.39660904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.40525267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4162234,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.42486703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.43351063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.44448137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4554521,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.4820479,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.49301863,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.6821808,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.66855055,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.67852396,"top":0.123703115,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.6878325,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.6974734,"top":0.12210695,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.70478725,"top":0.12210695,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"bounds":{"left":0.32779256,"top":0.2490024,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"49","depth":4,"bounds":{"left":0.33776596,"top":0.2490024,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.35006648,"top":0.2490024,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"bounds":{"left":0.359375,"top":0.2490024,"width":0.010305851,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.37167552,"top":0.2490024,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.3806516,"top":0.24740623,"width":0.00731383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"bounds":{"left":0.3879654,"top":0.24740623,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n return null;\n\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\nnamespace Jiminny\\Services\\Crm\\Hubspot;\n\nuse Carbon\\Carbon;\nuse Exception;\nuse Generator;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Illuminate\\Support\\Facades\\Cache;\nuse InvalidArgumentException;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ClientInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\FetchRelatedActivityInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\LayoutManagementInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\MatchCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\Provider\\HubspotInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityLookupInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\RemoteEntityManipulationInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SavePlaybackLinkToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SendSummaryToCrmInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SettingsInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmEntitiesInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\SyncCrmMetadataInterface;\nuse Jiminny\\Contracts\\Services\\Crm\\VerifyTaskExistsInterface;\nuse Jiminny\\Exceptions\\CrmException;\nuse Jiminny\\Exceptions\\HttpNotFoundException;\nuse Jiminny\\Jobs\\Crm\\NoteObject;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Contracts\\ActivityContract;\nuse Jiminny\\Models\\Crm\\BusinessProcess;\nuse Jiminny\\Models\\Crm\\Field;\nuse Jiminny\\Models\\Crm\\FieldData;\nuse Jiminny\\Models\\Crm\\Layout;\nuse Jiminny\\Models\\Crm\\Profile;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Playbook;\nuse Jiminny\\Models\\SocialAccount;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Repositories\\Crm\\CrmEntityRepository;\nuse Jiminny\\Repositories\\Crm\\FieldRepository;\nuse Jiminny\\Repositories\\Crm\\ProfileRepository;\nuse Jiminny\\Repositories\\ParticipantRepository;\nuse Jiminny\\Services\\Avatar\\ProspectPhotoPathService;\nuse Jiminny\\Services\\Crm\\BaseService;\nuse Jiminny\\Services\\Crm\\Hubspot\\Actions\\SyncArchivedProfilesAction;\nuse Jiminny\\Services\\Crm\\Hubspot\\Fields\\ValueNormalizer;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\OpportunitySyncTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncCrmEntitiesTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\SyncFieldsTrait;\nuse Jiminny\\Services\\Crm\\Hubspot\\ServiceTraits\\WriteCrmTrait;\nuse Jiminny\\Services\\Crm\\MatchDomainByEmailInterface;\nuse Jiminny\\Services\\Crm\\OpportunitySyncStrategyResolver;\nuse Jiminny\\Services\\Crm\\ResolveCompanyNameByEmailTrait;\nuse Jiminny\\Utils\\PlaybackUrlBuilder;\nuse Sentry;\nuse SevenShores\\Hubspot\\Exceptions\\BadRequest;\nuse Throwable;\nuse UnexpectedValueException;\n\n/**\n * @phpstan-type CrmFieldDefinition array{\n * name: string,\n * label: string,\n * description: string,\n * type: string,\n * fieldType: string,\n * hidden: bool,\n * showCurrencySymbol: bool,\n * options: array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }\n */\nclass Service extends BaseService implements\n HubspotInterface,\n SyncCrmEntitiesInterface,\n SyncCrmMetadataInterface,\n SendSummaryToCrmInterface,\n MatchDomainByEmailInterface,\n SavePlaybackLinkToCrmInterface,\n RemoteEntityManipulationInterface,\n FetchRelatedActivityInterface,\n LayoutManagementInterface,\n SettingsInterface,\n MatchCrmEntitiesInterface,\n RemoteEntityLookupInterface,\n VerifyTaskExistsInterface\n{\n use ResolveCompanyNameByEmailTrait;\n use SyncCrmEntitiesTrait;\n use WriteCrmTrait;\n use SyncFieldsTrait;\n use OpportunitySyncTrait;\n\n private const int ENGAGEMENT_BODY_MAX_LENGTH = 65536;\n\n private const string LOG_DATE_FORMAT = 'Y-m-d H:i:s';\n private const int BATCH_UPDATE_LIMIT = 100;\n private const string TEN_SECONDLY_ROLLING_POLICY = 'TEN_SECONDLY_ROLLING';\n private const int TEN_SECONDLY_ROLLING_LIMIT = 10;\n private const string CALLS_SEARCH_ENDPOINT = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n\n private const string TYPE_NOTE = 'NOTE';\n\n private const string TYPE_MEETING = 'MEETING';\n\n private const string TYPE_CALL = 'CALL';\n\n private const string API_URL = 'https://api.hubapi.com';\n\n // NB: v1 is legacy - v3 is the newest\n private const string ENDPOINT_PIPELINES = '/crm-pipelines/v1/pipelines/';\n private const string PIPELINE_OBJECT_TYPE_DEALS = 'deals';\n\n private const int TASK_VERIFICATION_CACHE_TTL = 86400; // 1 day\n\n /**\n * @var ClientInterface|Client\n */\n protected $client;\n protected OpportunitySyncStrategyResolver $opportunitySyncStrategyResolver;\n protected CrmEntityRepository $crmEntityRepository;\n protected ProspectPhotoPathService $prospectPhotoPathService;\n\n private SyncFieldAction $syncFieldAction;\n private PayloadBuilder $payloadBuilder;\n private SyncRelatedActivityManager $syncRelatedActivityManager;\n private SyncArchivedProfilesAction $syncArchivedProfilesAction;\n private WebhookSyncBatchProcessor $batchProcessor;\n\n public function __construct(\n Client $client,\n SyncFieldAction $syncFieldAction,\n PayloadBuilder $payloadBuilder,\n ProspectPhotoPathService $prospectPhotoPathService,\n SyncArchivedProfilesAction $syncArchivedProfilesAction,\n WebhookSyncBatchProcessor $batchProcessor,\n ) {\n parent::__construct();\n\n $this->client = $client;\n $this->syncFieldAction = $syncFieldAction;\n $this->prospectPhotoPathService = $prospectPhotoPathService;\n $this->payloadBuilder = $payloadBuilder;\n $this->syncArchivedProfilesAction = $syncArchivedProfilesAction;\n $this->batchProcessor = $batchProcessor;\n $this->opportunitySyncStrategyResolver = app(OpportunitySyncStrategyResolver::class, [\n 'client' => $this->client,\n ]);\n $this->syncRelatedActivityManager = app(SyncRelatedActivityManager::class, [\n 'client' => $this->client,\n 'payloadBuilder' => $this->payloadBuilder,\n 'logger' => $this->logger,\n ]);\n $this->crmEntityRepository = app(CrmEntityRepository::class);\n $this->dealFieldsService = app(DealFieldsService::class);\n }\n\n public function getDisplayName(): string\n {\n return 'HubSpot';\n }\n\n protected function getOAuthAccount(User $user): ?SocialAccount\n {\n // In this case, the Account Owner is always the connection for any API operations.\n $owner = $user->team->owner;\n\n return $owner->getSocialAccount(SocialAccount::PROVIDER_HUBSPOT);\n }\n\n public function getClient(): Client\n {\n /** @var Client */\n return $this->client;\n }\n\n /**\n * Convert raw field data into a format compatible with CRM APIs.\n *\n * @param bool $internal Direction of the conversion.\n * True is pulling from CRM, false normalize before sending to CRM.\n */\n public function normalizeValue(string $fieldType, string $fieldValue, bool $internal = false): string\n {\n return ValueNormalizer::normalize(\n fieldType: $fieldType,\n fieldValue: $fieldValue,\n isInbound: $internal,\n );\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultFields(string $activityType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n $defaultFields = FieldDefinitions::defaultTaskFields();\n\n // This lazy creates these fields if not already setup.\n foreach ($defaultFields as $defaultField) {\n $fields[] = $this->config->fields()->firstOrCreate($defaultField);\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityField(string $activityType): Field\n {\n /** @var Field $activityField */\n $activityField = $this->config->fields()->where([\n 'crm_provider_id' => 'activityType',\n 'object_type' => $activityType,\n ])->first();\n\n return $activityField;\n }\n\n /**\n * @inheritdoc\n */\n public function getSupportedPlaybookTypes(): array\n {\n return [Playbook::ACTIVITY_TYPE_TASK];\n }\n\n /**\n * @inheritdoc\n */\n public function getDefaultActivityLayoutFields(string $activityType, string $layoutType): array\n {\n $fields = [];\n\n if ($activityType === Playbook::ACTIVITY_TYPE_TASK) {\n // Outcome should always be provided calls/meetings.\n $fieldData = [\n [\n 'crm_provider_id' => $layoutType === Layout::TYPE_SOFTPHONE_SUMMARY ? 'disposition' : 'meetingOutcome',\n 'object_type' => Field::OBJECT_TASK,\n ],\n ];\n\n foreach ($fieldData as $data) {\n $field = $this->config->fields()->where($data)->first();\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n }\n\n return $fields;\n }\n\n public function getDealInsightsFields(): array\n {\n return FieldDefinitions::dealInsightsFields();\n }\n\n protected function getDefaultFollowupLayoutFields(string $activityType): array\n {\n $fields = [];\n $fieldRepo = app(FieldRepository::class);\n $fieldData = FieldDefinitions::followupFieldsFilter();\n\n foreach ($fieldData as $data) {\n $field = $fieldRepo->findOneConfigurationFieldByProperties($this->config, $data);\n\n // Only add the field if it is created, which it should be.\n if ($field) {\n $fields[] = $field;\n }\n }\n\n return $fields;\n }\n\n /**\n * @inheritdoc\n */\n public function syncField(Field $field): void\n {\n switch ($field->object_type) {\n case Field::OBJECT_ACCOUNT:\n $crmField = $this->client->getInstance()->companyProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_CONTACT:\n $crmField = $this->client->getInstance()->contactProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_OPPORTUNITY:\n $crmField = $this->client->getInstance()->dealProperties()->get($field->crm_provider_id);\n\n break;\n case Field::OBJECT_TASK:\n $this->syncSingleTaskField($field);\n\n return;\n default:\n return;\n }\n\n $this->syncFieldAction->execute($field, $crmField->toArray());\n }\n\n /**\n * @param array<array{\n * id:string,\n * label:string,\n * value?:string\n * }> $options\n *\n * @throws CrmException\n *\n * @return FieldData[]\n *\n */\n public function importPicklistValues(\n Field $field,\n array $options = [['id' => '', 'label' => '', 'value' => '']],\n ): array {\n if (! empty($options[0]['id']) || ! empty($options[0]['value'])) {\n // We already have the options, no need to fetch them again\n return $this->importOptions($field, $options);\n }\n\n $options = [];\n\n switch ($field->getObjectType()) {\n case Field::OBJECT_ACCOUNT:\n $options = $this->getClient()->fetchPropertyOptions('company', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_CONTACT:\n $options = $this->getClient()->fetchPropertyOptions('contact', $field->getCrmProviderId());\n\n break;\n\n case Field::OBJECT_OPPORTUNITY:\n // Hubspot has different endpoint for stages\n $options = $this->getClient()->fetchOpportunityFieldOptions($field);\n\n break;\n\n case Field::OBJECT_TASK:\n if ($field->getCrmProviderId() === 'disposition') {\n $options = $this->getClient()->fetchDispositionFieldOptions();\n } elseif (in_array($field->getCrmProviderId(), ['meetingOutcome', 'activityType'])) {\n $options = $this->getClient()->fetchMeetingOutcomeFieldOptions($field);\n }\n\n break;\n\n default:\n $this->logger->warning('Invalid object type', [\n 'object_type' => $field->getObjectType(),\n 'field_id' => $field->getId(),\n ]);\n\n throw new CrmException('Invalid object type');\n }\n\n return $this->importOptions($field, $options);\n }\n\n /**\n * @inheritdoc\n */\n public function importStages(?array $types = null, ?string $missingStageName = null): ?Stage\n {\n $missingStage = null;\n\n try {\n // Use the HubSpot API client instead of the SDK crmPipelines() method\n $endpoint = self::getDealsPipelinesEndpoint();\n $pipelinesResponse = $this->client->getInstance()->getClient()->request('GET', $endpoint);\n $pipelines = $pipelinesResponse->data->results;\n } catch (RequestException|BadRequest $exception) {\n throw $exception;\n }\n\n foreach ($pipelines as $pipeline) {\n $stages = [];\n\n // We create a business process to contain the pipeline, and store all stages against it.\n $p = ResponseNormalize::normalizePipeline($pipeline);\n\n // Create/update business process for this pipeline\n $businessProcess = $this->config->businessProcesses()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'type' => BusinessProcess::TYPE_OPPORTUNITY,\n 'is_selectable' => $p['active'],\n ]);\n\n // A record type is really a clone of the business process, used to store which record uses which pipeline.\n // Create/update record type clone\n $this->config->recordTypes()->updateOrCreate([\n 'crm_provider_id' => $p['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($p['label'], 0, 150),\n 'is_selectable' => $p['active'],\n 'business_process_id' => $businessProcess->id ?? null,\n ]);\n\n // Stages - fetch all existing stages upfront to avoid N+1 queries\n $existingStages = $this->config->stages()\n ->withTrashed()\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->get()\n ->keyBy('crm_provider_id');\n\n foreach ($p['stages'] as $dealStage) {\n $s = ResponseNormalize::normalizeDealStage($dealStage);\n\n /** @var ?Stage $existingStage */\n $existingStage = $existingStages->get($s['id']);\n\n // Restore soft-deleted stages that are now active in HubSpot\n if ($existingStage?->trashed() && $s['active']) {\n $existingStage->restore();\n }\n\n // Upsert stage (updates soft-deleted records without restoring them)\n $stage = $this->config->stages()->withTrashed()->updateOrCreate([\n 'crm_provider_id' => $s['id'],\n ], [\n 'team_id' => $this->team->id,\n 'name' => mb_strimwidth($s['label'], 0, 50),\n 'label' => mb_strimwidth($s['label'], 0, 191),\n 'type' => Stage::TYPE_OPPORTUNITY,\n 'sequence' => $s['displayOrder'],\n 'is_selectable' => $s['active'],\n 'probability' => $s['probability'] * 100,\n ]);\n\n if ($missingStageName === $s['id']) {\n $missingStage = $stage;\n }\n\n $stages[] = $stage->id;\n }\n\n $businessProcess->stages()->sync($stages);\n }\n\n return $missingStage;\n }\n\n /**\n * @inheritdoc\n */\n public function syncOrganization(): void\n {\n try {\n $endpoint = 'https://api.hubapi.com/integrations/v1/me';\n $response = $this->client->getInstance()->getClient()->request('get', $endpoint);\n\n $accountData = $response->data;\n $this->config->update(['default_currency' => $accountData->currency]);\n } catch (BadRequest $e) {\n throw new CrmException('Could not sync the organization.', $e->getCode(), $e);\n }\n }\n\n /**\n * @inheritdoc\n *\n * @throws CrmException\n */\n public function syncProfiles(?User $userToSearch = null): ?Profile\n {\n $this->syncArchivedProfilesAction->execute($this->team, $this->client, $this->config);\n\n try {\n $owners = $this->client->getOwners();\n } catch (\\HubSpot\\Client\\Crm\\Owners\\ApiException $e) {\n $this->logger->error('[HubSpot] Could not sync the profiles.', [\n 'team_id' => $this->team->getId(),\n 'reason' => $e->getMessage(),\n ]);\n\n throw new CrmException('Could not sync the profiles.', $e->getCode(), $e);\n }\n\n $profileRepository = app(ProfileRepository::class);\n $teamRepository = app(TeamRepository::class);\n\n foreach ($owners as $owner) {\n if ($owner->getArchived()) {\n // not supposed to fetch archived, but log anyway\n $this->logger->warning('[HubSpot] Found archived owner', [\n 'crm_provider_id' => $owner->getId(),\n 'email' => $owner->getEmail(),\n ]);\n\n continue;\n }\n\n $email = $owner->getEmail();\n if ($email === null) {\n continue;\n }\n\n $user = $teamRepository->findActiveTeamMemberByEmail($this->team, $email);\n\n if (! $user instanceof User) {\n continue;\n }\n\n $profile = $profileRepository->updateOrCreateProfile($user, [\n 'crm_configuration_id' => $this->config->getId(),\n 'crm_provider_id' => $owner->getId(),\n ]);\n\n if ($userToSearch && $userToSearch->getId() === $user->getId()) {\n return $profile;\n }\n }\n\n return null;\n }\n\n private function generateNameSearchPayload(string $name, int $offset, int $limit): array\n {\n $payload = [\n 'query' => $name,\n 'sorts' => [\n [\n 'propertyName' => 'modifieddate',\n 'direction' => 'DESCENDING',\n ],\n ],\n 'properties' => [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n 'industry',\n 'name',\n 'company',\n ],\n 'limit' => $limit,\n 'after' => $offset,\n ];\n\n $this->logger->debug('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n return $payload;\n }\n\n /**\n * @inheritdoc\n */\n public function find(string $name, array $scopes): array\n {\n $count = $this->limit ?? 20;\n $offset = $this->offset ?? 0;\n\n /** @var array<int, array<string, mixed>> */\n return Cache::remember(\n key: $this->team->getId() . $name . $count . $offset,\n ttl: 300,\n callback: function () use ($name, $offset, $count): array {\n $data = [];\n\n // Use the new V3 API to find contacts based on additional fields.\n foreach (['companies', 'contacts'] as $objectType) {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/' . $objectType . '/search';\n $payload = $this->generateNameSearchPayload($name, $offset, $count);\n $type = $objectType === 'companies' ? 'account' : 'contact';\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, [\n 'json' => $payload,\n ]);\n\n // Build mapped list.\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n\n $objectName = $this->buildContactName($properties);\n\n $record = [\n 'crmId' => $object['id'],\n // Pass crmUrl to the FE, needed for success message in the extension when you log activity.\n 'crmUrl' => $this->generateProviderUrl($object['id'], $type),\n 'name' => $objectName,\n 'prospectType' => $type,\n 'phoneNumbers' => [],\n ];\n\n if ($type === 'account') {\n $record['industry'] = $properties['industry'] ?? null;\n } else {\n $record['title'] = $properties['jobtitle'] ?? null;\n $record['organization'] = $properties['company'] ?? null;\n }\n\n $countryCode = $this->buildContactCountry($properties);\n $parsedNumber = $this->buildContactPhone($countryCode, $properties);\n\n // Add phone number to record.\n if (! empty($parsedNumber['phone'])) {\n $record['phoneNumbers'][] = [\n 'number' => $parsedNumber['phone'],\n 'nationalFormat' => phone_national($countryCode, $parsedNumber['phone']),\n 'type' => 'phone',\n ];\n }\n\n // Add mobile phone number to record.\n if (! empty($properties['mobilephone'])) {\n $mobileNumber = phone_e164($countryCode, $properties['mobilephone']);\n if ($mobileNumber !== null) {\n $record['phoneNumbers'][] = [\n 'number' => $mobileNumber,\n 'nationalFormat' => phone_national($countryCode, $mobileNumber),\n 'type' => 'mobile',\n ];\n }\n }\n\n $data[] = $record;\n }\n } catch (BadRequest $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->getUuid(),\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n\n throw $e;\n }\n }\n\n return $data;\n },\n );\n }\n\n\n /**\n * @inheritdoc\n */\n public function findOpportunities(?string $crmAccountId, ?string $crmContactId, ?int $userId = null): array\n {\n $data = [];\n $ownerData = [];\n $ownerId = null;\n\n if ($crmAccountId === null) {\n return $data;\n }\n\n if ($userId) {\n $profileRepository = app(ProfileRepository::class);\n $profile = $profileRepository->findProfileByUserId($this->config, $userId);\n\n $ownerId = $profile instanceof Profile ? $profile->getCrmProviderId() : null;\n }\n\n $closedStages = $this->getClosedDealStages();\n $payload = $this->payloadBuilder->generateOpportunitiesSearchPayload(\n $this->config,\n $crmAccountId,\n $closedStages,\n );\n\n $results = $this->client->getPaginatedData($payload, 'deals');\n\n foreach ($results['results'] as $object) {\n $properties = $object['properties'];\n\n $amount = null;\n if (empty($properties['amount']) === false) {\n $currency = $properties['deal_currency_code'] ?? $this->config->default_currency;\n\n // Values can contain commas and any junk so strip them.\n $value = (float) preg_replace('/[^\\d.]/', '', $properties['amount']);\n $amount = formatCurrency($value, $currency);\n }\n\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n\n if ($businessProcess === null) {\n // Import it.\n $stage = $this->importStages([Stage::TYPE_OPPORTUNITY], $properties['dealstage']);\n $businessProcess = $this->config\n ->businessProcesses()\n ->where('crm_provider_id', $properties['pipeline'])\n ->first();\n } else {\n $stage = $businessProcess\n ->stages()\n ->where('crm_provider_id', $properties['dealstage'])\n ->where('type', Stage::TYPE_OPPORTUNITY)\n ->first();\n\n if ($stage === null) {\n // Import it.\n $stage = $this->importStages(null, $properties['dealstage']);\n }\n }\n\n $recordType = null;\n if ($businessProcess) {\n $recordType = $businessProcess->recordTypes()->first();\n }\n\n $isWon = in_array($properties['dealstage'], $closedStages['won']);\n $isLost = in_array($properties['dealstage'], $closedStages['lost']);\n\n $record = [\n 'crmId' => $object['id'],\n 'name' => $properties['dealname'] ?? 'Unknown Deal',\n 'value' => $amount,\n 'won' => $isWon,\n 'closed' => $isWon || $isLost,\n 'stage' => [\n 'id' => $stage?->getUuid() ?? '',\n 'name' => $stage?->getName() ?? '',\n ],\n ];\n\n if ($recordType) {\n $record += [\n 'recordType' => [\n 'id' => $recordType->id_string,\n 'name' => $recordType->name,\n ],\n ];\n }\n\n if ($ownerId && isset($properties['hubspot_owner_id']) && $properties['hubspot_owner_id'] === $ownerId) {\n $ownerData[] = $record;\n }\n\n $data[] = $record;\n }\n\n if (! empty($ownerData)) {\n return $ownerData;\n }\n\n return $data;\n }\n\n /**\n * @inheritdoc\n */\n public function getTasks(?string $objectType, string $objectId, ?string $opportunityId): array\n {\n $data = [];\n switch ($objectType) {\n case 'contact':\n $hsObject = 'contact';\n\n break;\n case 'account':\n $hsObject = 'company';\n\n break;\n default:\n // This is a hack to prioritise and override a contact/company with a deal.\n if ($opportunityId) {\n $hsObject = 'deal';\n $objectId = $opportunityId;\n } else {\n throw new InvalidArgumentException('Object type not supported.');\n }\n }\n\n $engagementTypes = ['meetings', 'tasks'];\n\n foreach ($engagementTypes as $engagementType) {\n $payload = $this->payloadBuilder->getLinkToTaskPayload($hsObject, $objectId, $engagementType);\n\n $this->logger->info('[HubSpot] CRM Search requested', [\n 'request' => $payload,\n ]);\n\n $engagements = $this->client->getPaginatedData($payload, $engagementType);\n\n foreach ($engagements['results'] as $engagement) {\n if ($engagementType == 'meetings') {\n $title = $engagement['properties']['hs_meeting_title'] ?? 'Scheduled meeting';\n } elseif ($engagementType == 'tasks') {\n $title = $engagement['properties']['hs_task_subject'];\n } else {\n $title = 'Scheduled meeting';\n }\n\n $data[] = [\n 'crmId' => $engagement['id'],\n 'subject' => $title,\n 'due' => $engagement['properties']['hs_timestamp'],\n 'type' => $engagement['properties']['hs_activity_type'] ?? null,\n ];\n }\n }\n\n usort($data, function ($item1, $item2) {\n return $item2['due'] <=> $item1['due'];\n });\n\n return $data;\n }\n\n /**\n * Try to find CRM Objects using email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchExactlyByEmail(string $email, ?int $userId = null): ?array\n {\n $contactProperties = [\n 'email',\n 'firstname',\n 'lastname',\n 'country',\n 'phone',\n 'mobilephone',\n 'jobtitle',\n 'hubspot_owner_id',\n 'associatedcompanyid',\n 'photo',\n ];\n $contact = null;\n $account = null;\n\n try {\n $hsContact = $this->getClient()->getContactByEmail($email, $contactProperties);\n\n if ($hsContact) {\n $contact = $this->importContact($hsContact);\n $account = $contact->account;\n }\n\n $data = $this->convertCrmData($contact, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n } catch (BadRequest $e) {\n $this->logger->warning('[HubSpot] Search failed', [\n 'team_id' => $this->team->getId(),\n 'search_identifier' => $email,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return null;\n }\n\n public function getDomain(string $email): ?string\n {\n return $this->getDomainFromEmail($email);\n }\n\n /**\n * Try to find CRM objects using domain name of the email address\n *\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByDomain(string $domain, ?int $userId = null): ?array\n {\n $companyName = $domain;\n\n // Try to find a company matching their email domain.\n $companyProperties = [\n 'country',\n 'phone',\n 'name',\n 'hs_avatar_filemanager_key',\n 'industry',\n 'hubspot_owner_id',\n 'domain',\n ];\n\n try {\n $hsAccounts = $this->client\n ->getInstance()\n ->companies()\n ->searchByDomain($companyName, $companyProperties);\n } catch (Throwable $e) {\n $this->logger->info('[HubSpot] Search failed', [\n 'error' => $e->getMessage(),\n 'domain' => $domain,\n ]);\n\n return null;\n }\n\n $account = null;\n // If there are multiple accounts, don't guess, we'll ask later.\n if (\\count($hsAccounts->data->results) === 1) {\n // Persist this remote object.\n $account = $this->syncAccount($hsAccounts->data->results[0]->companyId);\n }\n\n $data = $this->convertCrmData(null, $account, $userId);\n\n return ! empty(array_filter($data)) ? $data : null;\n }\n\n /**\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n protected function convertCrmData(?Contact $contact, ?Account $account, ?int $userId = null): array\n {\n $countryCode = null;\n if ($contact && $contact->country_code) {\n $countryCode = $contact->country_code;\n } elseif ($account && $account->country_code) {\n $countryCode = $account->country_code;\n }\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact ? $contact->crm_provider_id : null,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n // If there are multiple opportunities, don't guess, we'll ask later.\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n protected function getCacheKey(string $object, ?int $userId = null): ?string\n {\n $key = $this->team->getId() . $object;\n $keySuffix = $this->getOwnerKeySuffix($userId);\n\n return $key . $keySuffix;\n }\n\n private function getOwnerKeySuffix(?int $userId = null): string\n {\n return $userId === null ? '' : (string) $userId;\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}\n */\n public function matchByPhone(string $phone, ?string $rawPhoneNumber = null, ?int $userId = null): ?array\n {\n if (str_contains($phone, '**')) {\n return null;\n }\n\n // trim all whitespaces if present so the lookup doesn't fail\n $phone = str_replace(' ', '', $phone);\n\n // Check if the user is internal.\n if ($this->isPhoneNumberOfTeamMember($phone)) {\n return null;\n }\n\n $response = $this->searchForPhoneNumber($phone);\n if (empty($response)) {\n return null;\n }\n\n // This would ideally importContact instead but the response type differs.\n $contact = $this->findAndSyncContact($response['results'][0]['id']);\n if (! $contact instanceof Contact) {\n return null;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account?->crm_provider_id,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n\n try {\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n } catch (Exception $e) {\n $this->logger->debug('[HubSpot] Opportunity failed to sync.', [\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n }\n\n private function isPhoneNumberOfTeamMember(string $phone): bool\n {\n $teamRepository = app(TeamRepository::class);\n $user = $teamRepository->findTeamMemberByPhone($this->team, $phone);\n\n if ($user instanceof User) {\n return true;\n }\n\n return false;\n }\n\n private function findAndSyncContact(string $crmId): ?Contact\n {\n try {\n return $this->syncContact($crmId);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'reason' => $exception->getMessage(),\n ]);\n\n return null;\n }\n }\n\n private function hasResults(array $response): bool\n {\n return isset($response['total']) && is_numeric($response['total']) && $response['total'] > 0;\n }\n\n private function searchForPhoneNumber(string $phone): array\n {\n // Normalizes the provided phone number for the API search.\n $normalizedPhone = $this->normalizePhoneNumber($phone);\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone);\n\n $this->logger->info('[HubSpot] Phone match search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($normalizedPhone, $payload);\n\n if (! $this->hasResults($response)) {\n $nationalPhone = preg_replace('/\\D/', '', phone_national(null, $phone));\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($nationalPhone);\n\n $this->logger->info('[HubSpot] Phone match national number search triggered', [\n 'phone' => $phone,\n 'nationalPhone' => $nationalPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n if (! $this->hasResults($response)) {\n $payload = $this->payloadBuilder->generatePhoneSearchPayload($normalizedPhone, true);\n\n $this->logger->info('[HubSpot] Phone match alternative search triggered', [\n 'phone' => $phone,\n 'normalizedPhone' => $normalizedPhone,\n 'payload' => $payload,\n ]);\n\n $response = $this->handlePhoneSearchRequest($phone, $payload);\n }\n\n return $this->hasResults($response) ? $response : [];\n }\n\n private function handlePhoneSearchRequest(string $phone, array $payload): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/contacts/search';\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n $endpoint,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Phone match failed', [\n 'phone' => $phone,\n 'reason' => $exception->getMessage(),\n ]);\n\n return [];\n }\n\n $this->logger->info('[HubSpot] Phone match completed', [\n 'phone' => $phone,\n 'response' => $response,\n ]);\n\n return $response->toArray();\n }\n\n private function normalizePhoneNumber(string $phone): string\n {\n return ltrim(phone_e164(null, $phone), '+0');\n }\n\n /**\n * @return null|array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n * }\n */\n public function matchByName(string $name, ?int $userId = null): ?array\n {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n return null;\n\n // Don't waste time searching for single character strings.\n if (\\strlen($name) <= 1) {\n return null;\n }\n\n $cacheKey = $this->getCacheKey($name, $userId);\n\n $result = Cache::remember($cacheKey, 60, function () use ($name, $userId) {\n $payload = $this->payloadBuilder->generateSearchContactsByNamePayload(\n $name,\n $this->getContactFields()\n );\n\n $hsContacts = $this->client->getPaginatedData($payload, 'contact');\n if (empty($hsContacts['results'])) {\n return false;\n }\n\n $contact = $this->importContact($hsContacts['results'][0]);\n if ($contact === null) {\n return false;\n }\n\n $account = $contact->account;\n $countryCode = $contact->country_code ?? $account->country_code ?? null;\n\n try {\n $hsOpportunities = $this->findOpportunities(\n $account ? $account->crm_provider_id : null,\n $contact->crm_provider_id,\n $userId\n );\n } catch (Exception $e) {\n $hsOpportunities = [];\n }\n\n $opportunity = null;\n $stage = null;\n if (! empty($hsOpportunities)) {\n // Persist this remote object.\n $opportunity = $this->syncOpportunity($hsOpportunities[0]['crmId']);\n $stage = $opportunity?->getStage();\n }\n\n return [\n null,\n $account,\n $opportunity,\n $contact,\n $stage,\n $countryCode,\n ];\n });\n\n return is_array($result) ? $result : null;\n }\n\n\n private function convertActivityAssociations(Activity $activity): array\n {\n return [\n 'contactIds' => $this->getParticipantsIds($activity),\n 'companyIds' => $activity->hasAccount() ? [$activity->account->crm_provider_id] : [],\n 'dealIds' => $activity->hasOpportunity() ? [$activity->opportunity->crm_provider_id] : [],\n 'ownerIds' => [],\n ];\n }\n\n private function getParticipantsIds(Activity $activity): array\n {\n $attendees = [];\n\n $participantRepository = app(ParticipantRepository::class);\n $participants = $participantRepository->getParticipantsWhoEnteredMeeting($activity);\n foreach ($participants as $participant) {\n if ($participant->user_id || $participant->isCoach()) {\n continue;\n }\n\n $contact = $participant->contact()->first();\n if ($contact && $contact->crm_provider_id) {\n $attendees[] = $contact->crm_provider_id;\n } else {\n if (! empty($participant->name)) {\n $attendeeData = $this->fetchMissingAttendeeInfo($participant);\n }\n if (! empty($attendeeData['id'])) {\n $attendees[] = $attendeeData['id'];\n }\n }\n }\n\n if ($activity->hasContact()) {\n $attendees[] = $activity->contact->crm_provider_id;\n }\n\n return array_unique($attendees);\n }\n\n private function fetchMissingAttendeeInfo(Participant $participant): array\n {\n // Check if we need to look inside an account context.\n $activity = $participant->getActivity();\n $companyId = $activity->hasAccount() ? $activity->getAccount()->crm_provider_id : null;\n\n // First check the local data.\n /** @var Contact[] $contacts */\n $contacts = $this->team->contacts()\n ->with('account')\n ->where('name', $participant->name)\n ->whereNotNull('email')\n ->get();\n\n foreach ($contacts as $contact) {\n // If we have a company in scope, check the contact is associated to it.\n if (\n $companyId !== null\n && ($contact->account_id === null || $companyId !== $contact->account->crm_provider_id)\n ) {\n continue;\n }\n\n return [\n 'id' => $contact->crm_provider_id,\n 'email' => $contact->email,\n ];\n }\n\n $payload = $this->generateNameSearchPayload($participant->name, 0, 20);\n\n try {\n $response = $this->client->getNewInstance()->crm()->contacts()->searchApi()->doSearch($payload);\n\n // TODO add some logic to choose the most suitable contact if multiple\n foreach ($response['results'] as $object) {\n $properties = $object['properties'];\n if (empty($object['properties']) === false) {\n // Check the company matches the contact.\n // Todo: Move this check inside the API search.\n if ($companyId !== null && $companyId !== $properties['associatedcompanyid']) {\n continue;\n }\n\n return [\n 'id' => $object['id'],\n 'email' => $properties['email'],\n ];\n }\n }\n } catch (Exception $e) {\n $this->logger->warning('[' . $this->getDisplayName() . '] Search failed', [\n 'teamId' => $this->team->id_string,\n 'request' => $payload,\n 'reason' => $e->getMessage(),\n ]);\n }\n\n return [];\n }\n\n /**\n * Store transcripts as note engagement.\n *\n * @throws Exception\n */\n public function createTranscriptNotes(Activity $activity): void\n {\n // For HS no need to check if Crm profile - Log Notes field is enabled\n // We only check if store_transcript toggle is enabled on crm profile.\n $engagement = [\n 'active' => true,\n 'ownerId' => $this->profile->crm_provider_id,\n 'timestamp' => $activity->created_at->tz($activity->user->timezone)->getTimestamp() * 1000,\n 'type' => 'NOTE',\n ];\n\n // Generate activity transcription.\n $transcriptionData = $this->generateTranscription($activity);\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $transcripts = mb_strimwidth($transcriptionData, 0, static::ENGAGEMENT_BODY_MAX_LENGTH);\n\n $metadata = [\n 'body' => $transcripts,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsEngagement = $this->client\n ->getInstance()\n ->engagements()\n ->create($engagement, $associations, $metadata);\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $noteId = $hsEngagement->data->engagement->id;\n\n // Store crm logged id in transcription.\n $transcription = $activity->getTranscription();\n $transcription->crm_activity_id = $noteId;\n $transcription->save();\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function updateRecord(string $objectType, string $objectId, array $data, array $headers = []): void\n {\n $payload = [\n 'properties' => $data,\n ];\n\n try {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n $this->client->getNewInstance()->crm()->deals()->basicApi()->update($objectId, $payload);\n\n break;\n case FieldData::OBJECT_CONTACT:\n $this->client->getNewInstance()->crm()->contacts()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_ACCOUNT:\n $this->client->getNewInstance()->crm()->companies()->basicApi()->update($objectId, $payload);\n\n break;\n\n case FieldData::OBJECT_TASK:\n // Endpoint for Engagements not ready\n $engagements = [\n 'type' => 'TASK',\n ];\n $metadata = $data;\n $this->client->getInstance()->engagements()->update($objectId, $engagements, $metadata);\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $objectId],\n $metadata,\n );\n\n break;\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n } catch (\\HubSpot\\Client\\Crm\\Deals\\ApiException $apiException) {\n $errorMessage = $apiException->getMessage();\n if ($apiException->getResponseBody()) {\n $responseBody = json_decode($apiException->getResponseBody(), true, 512, JSON_THROW_ON_ERROR);\n $errorMessage = $responseBody['message'] ?? $apiException->getMessage();\n }\n\n $this->logger->error(\n '[HubSpot] Update record failed',\n [\n 'objectType' => $objectType,\n 'objectId' => $objectId,\n 'payload' => $payload,\n 'reason' => $errorMessage,\n 'team' => $this->team->getUuid(),\n ]\n );\n\n throw new CrmException($errorMessage);\n }\n }\n\n /*\n * @inheritdoc\n */\n public function getRecord(string $objectType, string $objectId, array $fields = []): array\n {\n switch ($objectType) {\n case FieldData::OBJECT_OPPORTUNITY:\n return $this->client->getInstance()->deals()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_CONTACT:\n return $this->client->getInstance()->contacts()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_ACCOUNT:\n return $this->client->getInstance()->companies()->getById($objectId)->toArray();\n\n case FieldData::OBJECT_TASK:\n return $this->client->getInstance()->engagements()->get($objectId)->toArray();\n\n default:\n throw new UnexpectedValueException('Unsupported object type \"' . $objectType . '\"');\n }\n }\n\n /**\n * @throws BadRequest\n * @throws CrmException\n */\n public function updateStage($crmObject, Stage $stage): void\n {\n $payload = [\n 'properties' => [\n [\n 'name' => 'dealstage',\n 'value' => $stage->crm_provider_id,\n ],\n ],\n ];\n\n try {\n $this->client->getInstance()->deals()->update($crmObject->crm_provider_id, $payload);\n } catch (BadRequest $badRequest) {\n if ($badRequest->getCode() === 403) {\n throw new CrmException(\n \"Sorry, you don't have permission to update this stage.\",\n $badRequest->getCode(),\n $badRequest,\n );\n }\n\n $this->logger->warning('[HubSpot] Stage update failed', [\n 'dealId' => $crmObject->crm_provider_id,\n 'payload' => $payload,\n 'message' => $badRequest->getMessage(),\n ]);\n\n throw $badRequest;\n }\n }\n\n public function generateProviderUrl(string $providerId, string $objectType): ?string\n {\n $url = null;\n $baseUrl = 'https://app.hubspot.com/contacts/' . $this->config->crm_provider_id . '/';\n\n switch ($objectType) {\n case 'account':\n $url = $baseUrl . 'company/' . $providerId;\n\n break;\n\n case 'contact':\n $url = $baseUrl . 'contact/' . $providerId;\n\n break;\n\n case 'opportunity':\n $url = $baseUrl . 'deal/' . $providerId;\n\n break;\n\n case 'task':\n case 'activity':\n return null;\n\n // This should not be deep-linked as per JMNY-3934.\n //$url = $baseUrl.'tasks/list/view/all/?taskId='.$providerId;\n break;\n }\n\n return $url;\n }\n\n public function searchCalls(Carbon $from, Carbon $to, string $activityProvider): array\n {\n $this->logger->info('[HubSpot] Search calls', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $calls = [];\n $page = 1;\n\n do {\n try {\n $payload = $this->payloadBuilder->generateGetCallsPayload($from, $to, $activityProvider, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n $calls = array_merge($calls, $responseResults);\n $page++;\n } while (! empty($responseResults));\n\n return $calls;\n }\n\n public function searchCallsForPeriodByPage(Carbon $from, Carbon $to, int $page, bool $retry = true)\n {\n try {\n $payload = $this->payloadBuilder->generateSearchCallsByPeriodPayload($from, $to, $page);\n $response = $this->client->getInstance()->getClient()->request(\n 'POST',\n self::CALLS_SEARCH_ENDPOINT,\n ['json' => ($payload)],\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search calls for period failed', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallsForPeriodByPage($from, $to, $page, false);\n }\n $response = null;\n }\n\n return $response;\n }\n\n public function searchCallsForPeriod(Carbon $from, Carbon $to): Generator\n {\n $this->logger->info('[HubSpot] Search calls for period', [\n 'from' => $from->format(self::LOG_DATE_FORMAT),\n 'to' => $to->format(self::LOG_DATE_FORMAT),\n ]);\n\n $page = 1;\n\n do {\n $response = $this->searchCallsForPeriodByPage($from, $to, $page);\n\n $responseResults = empty($response['results']) ? [] : $response['results'];\n\n $associationContacts = $this->getAssociationDataForCollection($responseResults, 'calls', 'contacts');\n $associationCompanies = $this->getAssociationDataForCollection($responseResults, 'calls', 'companies');\n $associationDeals = $this->getAssociationDataForCollection($responseResults, 'calls', 'deals');\n\n foreach ($responseResults as $call) {\n $call['associations'] = [\n 'contacts' => $this->importAssociationData($call, $associationContacts),\n 'companies' => $this->importAssociationData($call, $associationCompanies),\n 'deals' => $this->importAssociationData($call, $associationDeals),\n ];\n\n yield $call;\n }\n $page++;\n } while (! empty($responseResults));\n }\n\n public function getCall(string $callId): array\n {\n $this->logger->info('[HubSpot] Get call', [\n 'call_id' => $callId,\n ]);\n\n $searchAttributes = $this->payloadBuilder->getSearchCallAttributes();\n $endpoint = sprintf(\n 'https://api.hubapi.com/crm/v3/objects/calls/%s',\n $callId,\n );\n\n try {\n $response = $this->client->getInstance()->getClient()->request(\n 'GET',\n $endpoint,\n [],\n sprintf(\n 'properties=%s&associations=contacts,companies,deals',\n implode(',', $searchAttributes),\n ),\n );\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Get call failed', [\n 'call_id' => $callId,\n 'reason' => $exception->getMessage(),\n ]);\n $response = null;\n }\n\n return empty($response) ? [] : $response->toArray();\n }\n\n public function bulkAddPlaybackURLToDescription(array $crmUpdateData): array\n {\n $crmUpdateBatches = array_chunk($crmUpdateData, self::BATCH_UPDATE_LIMIT);\n\n $updatedCrmIds = [];\n\n foreach ($crmUpdateBatches as $crmBatch) {\n $payload = $this->payloadBuilder->generatePlaybackAddUrlBatchPayload($crmBatch);\n $updateSuccess = $this->bulkAddPlaybackURLToDescriptionRequest($payload);\n if ($updateSuccess) {\n $updatedCrmIds = array_merge($updatedCrmIds, array_column($crmBatch, 'crm_id'));\n }\n }\n\n return $updatedCrmIds;\n }\n\n private function bulkAddPlaybackURLToDescriptionRequest(array $payload, bool $retry = true): bool\n {\n try {\n $this->client->getNewInstance()->crm()->objects()->batchApi()->update('calls', $payload);\n\n return true;\n } catch (\\HubSpot\\Client\\Crm\\Objects\\ApiException $e) {\n $response = json_decode($e->getResponseBody(), true);\n $retryAfter =\n isset($response['policyName'])\n && $response['policyName'] == self::TEN_SECONDLY_ROLLING_POLICY\n ? self::TEN_SECONDLY_ROLLING_LIMIT\n : 1;\n } catch (Exception $e) {\n $retryAfter = 1;\n }\n\n $this->logger->warning('[HubSpot] Bulk add playback url to CRM failed', [\n 'reason' => $e->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep($retryAfter);\n\n return $this->bulkAddPlaybackURLToDescriptionRequest($payload, false);\n }\n\n return false;\n }\n\n /**\n * Sometimes we have secondly rate limit error, then retry request after 1 second\n */\n public function searchCallByRecordingURLToken(string $playbackURLToken, bool $retry = true): array\n {\n $endpoint = 'https://api.hubapi.com/crm/v3/objects/calls/search';\n $payload = $this->payloadBuilder->generateSearchCallByTokenPayload($playbackURLToken);\n\n $this->logger->info('[HubSpot] CRM Search by playback URL token requested', [\n 'request' => $payload,\n ]);\n\n try {\n $response = $this->client->getInstance()->getClient()->request('POST', $endpoint, ['json' => ($payload)]);\n } catch (Exception $exception) {\n $this->logger->info('[HubSpot] Search by playback URL token failed', [\n 'playbackURLToken' => $playbackURLToken,\n 'reason' => $exception->getMessage(),\n 'retry' => $retry,\n ]);\n\n if ($retry) {\n sleep(1);\n\n return $this->searchCallByRecordingURLToken($playbackURLToken, false);\n }\n\n return [];\n }\n\n return empty($response['results']) ? [] : $response['results'][0];\n }\n\n /**\n * Generate transcription for the activity.\n */\n private function generateTranscription(Activity $activity): string\n {\n if (! $this->config->store_transcript) {\n // If sending transcription to activity toggle is disabled\n return '';\n }\n\n $transcriptionSegments = $this->transcriptionService->findTranscriptionByActivity($activity);\n\n if ($transcriptionSegments->isEmpty()) {\n return '';\n }\n\n $transcription = sprintf(\n '<p><strong>Transcript for %s</strong></p><p></p>',\n $activity->title ?? $activity->activity_title,\n );\n\n $roomOwnerParticipant = $activity->findParticipantRoomOwner();\n $roomOwnerParticipantId = $roomOwnerParticipant !== null\n ? $roomOwnerParticipant->getId()\n : null;\n\n\n $transcription .= $transcriptionSegments\n ->map(static function (array $transcriptionSegment) use ($roomOwnerParticipantId): string {\n $isOrganiser = $roomOwnerParticipantId === $transcriptionSegment['participantId']\n && $roomOwnerParticipantId !== null;\n $transcriptColor = $isOrganiser ? '#000000' : '#f0415a';\n\n return sprintf(\n '<span style=\"color: %s;\">%s | </span>%s',\n $transcriptColor,\n $transcriptionSegment['formattedStartsAt'],\n $transcriptionSegment['transcript'],\n );\n })\n ->implode('<br />');\n\n return $transcription;\n }\n\n /**\n * @param array<array{\n * id: string,\n * label: string,\n * value?: string,\n * }> $options\n *\n * @return FieldData[]\n */\n private function importOptions(Field $field, array $options): array\n {\n $fieldValues = [];\n $values = [];\n $sequence = 0;\n\n foreach ($options as $option) {\n $values[] = [\n 'value' => $option['value'] ?? $option['id'],\n 'label' => substr($option['label'], 0, 255),\n 'sequence' => $sequence++,\n ];\n }\n\n $fieldsToPurge = $field->values()->get()->pluck('value')->toArray();\n\n foreach ($values as $value) {\n $value['value'] = substr($value['value'], 0, 255);\n $fieldValues[] = $field->values()->updateOrCreate([\n 'value' => $value['value'],\n ], $value);\n\n // Remove this value from the ones we are going to purge.\n if (($key = array_search($value['value'], $fieldsToPurge, false)) !== false) {\n unset($fieldsToPurge[$key]);\n }\n }\n\n // Delete the old values that are no longer used.\n $field->values()->whereIn('value', $fieldsToPurge)->delete();\n\n return $fieldValues;\n }\n\n public function saveTranscriptionSummaryAsNote(\n ActivityContract $activity,\n string $title,\n string $body,\n ?string $objectId,\n ?NoteObject $noteObject = null,\n ): ?string {\n if ($noteObject === null || $objectId === null) {\n return null;\n }\n\n /** @var User $user */\n $user = $activity->getUser();\n\n $profile = $this->assignCrmOwner($user, $activity);\n if (! $profile instanceof Profile) {\n return null;\n }\n\n $timestamp = Carbon::now($user->getTimezone())->getTimestamp() * 1000;\n $engagement = [\n 'active' => true,\n 'ownerId' => $profile->getAttribute('crm_provider_id'),\n 'timestamp' => $timestamp,\n 'type' => 'NOTE',\n ];\n\n // Truncate Notes with max notes length because transcription text could be very long.\n $body = mb_strimwidth($body, 0, self::ENGAGEMENT_BODY_MAX_LENGTH);\n $metadata = [\n 'body' => $body,\n ];\n\n $associations = $this->convertActivityAssociations($activity);\n\n try {\n $hsActivityId = $this->client->createNote(\n body: $body,\n ownerId: $profile->getCrmProviderId(),\n timestamp: $timestamp,\n objectId: $objectId,\n noteObject: $noteObject,\n );\n\n $this->logCrmEngagementManipulation(self::ACTION_CREATE, $engagement, $metadata, $associations);\n\n $this->logger->info('[HubSpot] Saving Transcription Summary as Note', [\n 'activity' => $activity->getUuid(),\n 'crmActivity' => $hsActivityId,\n ]);\n\n return $hsActivityId;\n } catch (Exception $e) {\n Sentry::captureException($e);\n }\n\n return null;\n }\n\n public function attachSummaryToActivity(ActivityContract $activity, string $summaryTitle, string $summaryContents): bool\n {\n $this->logger->info('[HubSpot] Attaching summary to activity', [\n 'activity' => $activity->getUuid(),\n 'summary_content' => $summaryContents,\n ]);\n\n if (! $activity instanceof Activity) {\n throw new InvalidArgumentException('Expected instance of Activity');\n }\n\n $summary = '<p><strong>' . $summaryTitle . '</strong></p>';\n $summary .= '<p>' . $summaryContents . '</p>';\n $metadata = $this->buildMetadataForSummaryUpdate($activity, $summary);\n\n try {\n $type = $this->matchActivityEngagementType($activity);\n $engagement = ['type' => $type];\n\n $this->client->updateEngagement($activity->getCrmProviderId(), $engagement, $metadata);\n } catch (Exception $e) {\n $this->logger->warning('[HubSpot] Update summary failed', [\n 'activity' => $activity->getUuid(),\n 'reason' => $e->getMessage(),\n ]);\n\n return false;\n }\n\n $this->logCrmEngagementManipulation(\n self::ACTION_UPDATE,\n ['crmId' => $activity->getCrmProviderId()],\n $metadata,\n );\n\n return true;\n }\n\n private function buildMetadataForSummaryUpdate(Activity $activity, string $summary): array\n {\n $descriptionField = $activity->getType() === Activity::TYPE_CONFERENCE ? 'internalMeetingNotes' : 'body';\n $engagement = $this->client->getEngagementData($activity->getCrmProviderId());\n // Meeting without internalMeetingNotes might mean it just does not have any notes;\n $description = $engagement['metadata'][$descriptionField] ?? null;\n\n if (empty($description)) {\n $data = $summary;\n } else {\n // avoid playbook url link to Jiminny being sent twice in the activity description\n $targetUrl = PlaybackUrlBuilder::build($activity);\n\n if (str_contains($description, $targetUrl)) {\n $jiminnyUrl = '<p><a href=\"' . $targetUrl . '\" title=\"Play at Jiminny\">Play at Jiminny</a></p>';\n $summary = str_replace($jiminnyUrl, '', $summary);\n\n $this->logger->info('[HubSpot] Summary modified', [\n 'activity' => $activity->getUuid(),\n 'target_url' => $jiminnyUrl,\n 'modified_summary_content' => $summary,\n ]);\n }\n\n $data = $description . '<p></p>' . $summary;\n }\n\n return [\n $descriptionField => $data,\n ];\n }\n\n public function fetchAndAssociateRelatedActivity(Activity $activity): ?Activity\n {\n return $this->syncRelatedActivityManager->fetchAndAssociateRelatedActivity($activity);\n }\n\n public function fetchRelatedActivity(Activity $activity): array\n {\n return [];\n }\n\n public function getDealsInBulk(array $dealIds): array\n {\n $payload = $this->payloadBuilder->getDealsInBulkPayload($dealIds);\n\n return $this->client->getPaginatedData($payload, 'deals');\n }\n\n /**\n * Extract deal IDs from HubSpot search response.\n *\n * @param array $hubspotResponse The raw HubSpot search API response.\n * @param bool $includeArchived Whether to include archived deals (default: false).\n *\n * @return string[] Array of deal IDs as strings.\n */\n public function extractDealIds(array $hubspotResponse, bool $includeArchived = false): array\n {\n if (empty($hubspotResponse['results'])) {\n return [];\n }\n\n return array_values(\n array_map(\n fn ($deal) => $deal['id'],\n array_filter(\n $hubspotResponse['results'],\n fn ($deal) => $includeArchived || empty($deal['archived'])\n )\n )\n );\n }\n\n public function matchActivityEngagementType(Activity $activity): string\n {\n return match ($activity->getType()) {\n Activity::TYPE_CONFERENCE => self::TYPE_MEETING,\n Activity::TYPE_SOFTPHONE, Activity::TYPE_SOFTPHONE_INBOUND => self::TYPE_CALL,\n default => self::TYPE_NOTE,\n };\n }\n\n private function assignCrmOwner(User $user, ActivityContract $activity): ?Profile\n {\n $profile = $user->getProfile();\n if ($profile instanceof Profile) {\n return $profile;\n }\n\n $this->logger->info('[HubSpot] Unable to save summary. No profile', [\n 'activity' => $activity->getUuid(),\n ]);\n\n return null;\n }\n\n private static function getDealsPipelinesEndpoint(): string\n {\n return self::API_URL . self::ENDPOINT_PIPELINES . self::PIPELINE_OBJECT_TYPE_DEALS;\n }\n\n public function verifyTaskExists(Activity $activity): bool\n {\n $crmProviderId = $activity->getCrmProviderId();\n $cacheKey = \"crm_task_exists:{$this->config->getId()}:$crmProviderId\";\n\n return Cache::remember($cacheKey, self::TASK_VERIFICATION_CACHE_TTL, function () use ($crmProviderId) {\n try {\n $engagement = $this->client->getEngagementData($crmProviderId);\n\n return ! empty($engagement);\n } catch (HttpNotFoundException|BadRequest) {\n // Engagement not found in CRM - this is expected and permanent\n $this->logger->info('[Hubspot] Engagement not found during verification', [\n 'engagement_id' => $crmProviderId,\n 'config_id' => $this->config->getId(),\n ]);\n\n return false;\n }\n // Let other exceptions (network errors, rate limits, etc.) bubble up for retry\n });\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":true,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"bounds":{"left":0.011968086,"top":0.047885075,"width":0.024268618,"height":0.024740623},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-5081709979380413320
|
4074613745204205637
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
[URL_WITH_CREDENTIALS] 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) {
...
|
4843
|
NULL
|
NULL
|
NULL
|
|
4849
|
176
|
2
|
2026-05-07T14:33:16.693033+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164396693_m2.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"bounds":{"left":0.064494684,"top":0.019952115,"width":0.040226065,"height":0.025538707},"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"bounds":{"left":0.8081782,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"bounds":{"left":0.8234708,"top":0.019952115,"width":0.09208777,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9155585,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"bounds":{"left":0.9268617,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"bounds":{"left":0.9381649,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"bounds":{"left":0.96609044,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"bounds":{"left":0.9773936,"top":0.019952115,"width":0.011303191,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"bounds":{"left":0.9886968,"top":0.019952115,"width":0.011303186,"height":0.025538707},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"bounds":{"left":0.39660904,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"bounds":{"left":0.40525267,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"bounds":{"left":0.4162234,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"bounds":{"left":0.42486703,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"bounds":{"left":0.43351063,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"bounds":{"left":0.44448137,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"bounds":{"left":0.4554521,"top":0.09896249,"width":0.024268618,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"bounds":{"left":0.4820479,"top":0.09896249,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"bounds":{"left":0.49301863,"top":0.09896249,"width":0.029587766,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"bounds":{"left":0.6821808,"top":0.09896249,"width":0.02825798,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"bounds":{"left":0.66855055,"top":0.123703115,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"}]...
|
-1813933769748308613
|
-8168392714296784052
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
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
PhostormProledeyINavigarecodeLukas sterka 121 • nov100% 12Thu 7 May 17:33:16FV faVsco.js?9 master k=custom.logElaravel.logA SF (jiminny@localhost]4 HS_local (iminny@localhost] >iti accounts (jiminny@localhost]CascadeD CrmObjects© RateLimitException.php© PaginationState.phpA console (PROD]# console [eu)A console [STAGING]Hubspot Rate LimitingHubSpot Search Rate+0 ..> • DecorateActivityTx: Auto liminnyna DummySearched searchicrm/v3/obiects/*/search in ~/iminnvlaop ( oho) (613)>• Helpers© HubT OpportunitySyncTrait.phpSELECT * FRUM crm_tieLd_data WhERE crm_layout_entity_1d IN (6494,6495,64%6,6497,0498,0499); m 06 41 x6 Av D HubspotginatedData|searchCallBy in app/Services/Crm/Hubspot (*-php) (8)AccountSyncStrate>@ Actions_ contactsyncstrate•DDIe• U rielas• W Journa.mportopportunity8atch.onoImportBatchJobTrait.pho(C) Hubspot/Service.php x (C) PayloadBuilder.pho© MatchActivityCrmData.phpC) CrmActivityService.phoCachedCrmServiceDecorator.phpTHubspot/.SyncCrmEntitiesTrait.php(C) Pipedrive/Service.php1Servicelnterface.oho@ OpportunitvSvncTest.ohnclass Service extends BaseService implementsA8 A49 V1 X33 41 A37 VISELECTCONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE "' END) AS user_id,Searched crm/v3/objects.*search|request.*POST.*search|getPaginatedData|searchCallBy and performed 1 other queryu.emarsa.*searched crm/vs/obiects.searchpost.crm/v3.request. Post|search. enapointin app/services/erm/Hubspot.pnp3t.owner 1d FROM social accounts saThoughtsTnoughtsJOIN users uMetadataon v.id = sa.sociable_id• C OpportunitvSvncSti 1506 C'public function updateStage($crm0bject, Stage $stage): voidt...}JOIN teams t .n<->1: on t.id = u.team_idu ConcernsWHERE u.team_id = 2 and sa.provider = 'hubspot':oto rast Context Find all Hubspot search APl calls including crm/vs/objects//search enapoints and request metnoas ...in 3.Z8S© HubspotLastMoi 1538 @public function generateProviderUrl(string $providerid, string $objecttype): ?stringt...*select * from social accounts whene id = 1499•(C) HubspotlastMoi 10/eBased on my analvsis, here are all Hubspot search API calls in the codebase:© HubspotLastMoi 1571 Gpublic function searchCalls(Carbon $from, Carbon $to, string SactivityProvider): arrayselect * from opportunities where team_id = 2(C) HubspotlastMoi 10/4© HubspotLastMoi 1573c) Huosootsindlest 10/4Sthis->logger->info('[HubSpot] Search calls', ['from' => $from->format( format: self::L0G_DATE_FORMAT).itol => Sto->fonmatd fonat: self::L0G_DATE_FORMAT),and crm_provider_id IN ('51317301383');Summary of Alll HubSoot Search AP calls1. Pagination Service (HubspotPaqinationService.oho line 36)A ohoC. HubsnotSvncStr 15/select * from contacts where id = 85;(C) HubsnotWebhoc 15/0• M Padinationi(C) HubsootPadinati 15/9(c) PadinationConfit(c) DadinationState 1500M ProcnectSoarchStr.D);Scalls = []:Spage = 1;do {Sendpoint = Client::BASE URL . "/crm/v3/obiects/Stvoe?/search".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 = 15057Used by opportunity sync strategies to paginate through deals/companies/contacts2. Service.php - Multiple search endpointsMethod• M Pedictry {SELECT * FROM crm_fields WHERE id IN (1405, 1407. 1972, 2128):• M ServiceTraitsT OpportunitySync 1584Saoone - Stis-20/ont -3genstoner) getttentev neudkser, Sto, SaetivayProvscen -nand LepnonesearchkequestSresponse = sth1s->cl1ent->qetinstanceo->qetul1ento->requesto61T SyncCrmEntities 1583method: "Pusl",+ CuncCioldeTroit 11586nt: self::CALLS SEARCH ENDPOINT.select * rrom reatuselect * from team features where team id IN (1):select * from team features where feature id IN (36):searchenusrorper odbypageu© WriteCrmTrait.pl 1587f'ison'=oavload).EndpointPurposecrm/v3/obiects/companies,contacts)/searchName search for contacts/companiescrm/vs/ob1ects/contacts/search/crm/v3/001ects/calts/search/crm/v3/001ects/calts/search/ero/v2/ohfects/calls/searchlImport calls by periodPaginated call searchFind call by recording URL> O Utils|SHOW CREATE TABLE opportunity_contacts;SELEC * FROM opportunity contactsWHERE crm provider 1d = 111751'4> 0 Webhook|© BatchSyncCollectoi 159€© BatchSyncRedisSer 1591c) Client.php© ClosedDealStagesS 1593catch(Excention Sexcention) <Sthis->loager->info('[HubSpotl Search calls failed'. [at: self::LOG DATE FORMAT)'to' => sto->format foiseLf::LOG DATE FORMAT) .I"reason' => Sexcention->aetMessadeOll"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)•@ DealFieldsService.r 1594© DecorateActivitv.ot 1595Snesnonse = null.© FieldDefinitions.ohr 1596# hubsnot wehhook metricsl€ FieldTvoeConvertei 1597@ HubspotClientinterl 159875C) Hubsoot TokenMan.C) PavloadBuilder.oho 100k© RemoteCrmObiect\ 1601SresponseResults = empty($response['results']) ? (] : $response['results'];Scalls = array_merge($calls, $responseResults);$page++;} while (! empty($responseResults));select * from opportunities where team_id = 2 and crm_provider_id IN ('374720564', '14527423589' , •49908861993', '5041. No Retry-After header in search responses - Your debua loa confirms this. HubSoot doesn't send rate limit quidance in searchiSELECT * FROM opportunity contacts WHERE opportunity id = '414';SELECT * FROM opportunity contacts WHERE crm_provider id = '131501';colort + fnom contante whono id in (414LAA•9) ResnonceNormalize 1602C) Service nhnreturn Scalls:select * from activities where crm confiquration id = 2:@ SvncFieldAction nh 1604@ SvncPelatedActivits 1605select settings from crm confiqurations where id = 11;2. PavloadRuilder nhn denerates navloads for.generate0pportunitySearchPayload() - Deal search for sync strategiesaenerateSearchContactsRvamePavload/ - Contact name searchgeneratePhoneSearchPayload - Phone searchgenerateSearchCal1cBvPeriodPavload( - Call period searchljenerateGetCallsPayload() - Call import searchaenerateSearchCal1BvTokenPavload ()) - Call by token searchKey FindingsAPI responses.2. Rate limits are enforced server-side.• ten secondly rolling → 10 requests per 10 seconds• secondly → 1 request per second (for search endpoints)Ask anvthina (&4D)< CodeC° AdantivearQube 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)W Windsurf Toams 1581-76UTF.8io 4 spaces...
|
4843
|
NULL
|
NULL
|
NULL
|
|
4850
|
175
|
5
|
2026-05-07T14:33:17.926255+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-07/1778 /Users/lukas/.screenpipe/data/data/2026-05-07/1778164397926_m1.jpg...
|
PhpStorm
|
faVsco.js – Hubspot/Service.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
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
8
49
1
33
1
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"master, menu","depth":5,"on_screen":true,"help_text":"Git Branch: master<br/>Some incoming commits are not fetched<br/>","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Start Listening for PHP Debug Connections","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"AskJiminnyReportActivityServiceTest","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Run 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Debug 'AskJiminnyReportActivityServiceTest'","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"More Actions","depth":6,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"JetBrains AI","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Search Everywhere","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"IDE and Project Settings","depth":5,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Execute","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Explain Plan","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Browse Query History","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"View Parameters","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open Query Execution Settings…","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"In-Editor Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Tx: Auto","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Cancel Running Statements","depth":4,"on_screen":true,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Playground","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"jiminny","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"6","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","depth":4,"on_screen":true,"value":"# **************************** HS **************************************\n\nselect * from teams where id = 2; # 2\nselect * from features; # 2\nselect * from team_features where team_id = 2; # 2\nselect * from crm_configurations where id = 2; # 2\nselect * from users where team_id = 2; #\nselect * from playbooks where team_id = 2; # event 38\nselect * from playbook_categories where playbook_id = 38; #\n\nSELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;\nhttps://app.hubspot.com/contacts/4392066/deal/16964514951/?engagement=96069102624\n https://app.staging.jiminny.com/playback/d5df34dc-bd66-4ff5-a7b3-8d3be30322a0\n\nSELECT * FROM activities WHERE uuid_to_bin('04fdcd0d-818f-4c53-92dc-6f18bc753ffd') = uuid;\n# 609126 softphone tr. 11241\n\nSELECT * FROM activities WHERE uuid_to_bin('6521bfcd-5a30-46e5-9f74-5440fd48befd') = uuid;\n# 608874 conference tr. 11226 crmId: 103422236596\n\nselect * from ai_prompts where transcription_id IN (11241, 11226);\nselect * from activity_summary_logs where activity_id = 608874;\n\nselect * from sidekick_settings;\nselect * from default_activity_types;\n\nselect * from crm_field_data where activity_id = 1223;\n\nselect * from crm_layouts where crm_configuration_id = 2;\nSELECT * FROM crm_layout_entities WHERE crm_layout_id IN (554);\nselect * from crm_fields where crm_configuration_id = 11 and object_type = 'event';\nSELECT * FROM crm_field_values WHERE crm_field_id IN (1455,1450);\n\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id = 971;\nSELECT * FROM crm_field_data WHERE crm_layout_entity_id IN (6494,6495,6496,6497,6498,6499);\n\nSELECT\n CONCAT(u.id, CASE WHEN u.id = t.owner_id THEN ' (owner)' ELSE '' END) AS user_id,\n u.email,\n sa.*,\n t.owner_id FROM social_accounts sa\nJOIN users u\n on u.id = sa.sociable_id\nJOIN teams t on t.id = u.team_id\nWHERE u.team_id = 2 and sa.provider = 'hubspot';\n\nselect * from social_accounts where id = 1499;\n\nselect * from opportunities where team_id = 2\nand crm_provider_id IN ('51317301383');\n\nselect * from contacts where id = 85;\n\nselect * from opportunities where team_id = 2 order by id desc;\nselect * from opportunities where team_id = 2 and crm_provider_id = '51317301383'; # 5112\nselect * from opportunities where team_id = 2 and crm_provider_id = '55976759904'; # 5112\nselect * from opportunity_contacts where opportunity_id = 5117;\nselect * from crm_field_data where object_id = 1365;\nSELECT * FROM crm_fields WHERE id IN (1405, 1407, 1972, 2128);\n\nselect * from features;\nselect * from team_features where team_id IN (1);\nselect * from team_features where feature_id IN (36);\n\nSHOW CREATE TABLE opportunity_contacts;\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '111751';\n\n# $slug = 'HUBSPOT_WEBHOOK_SYNC';\n# $team = Jiminny\\Models\\Team::find(2);\n# $feature = Feature::query()->where('slug', $slug)->first();\n# TeamFeature::query()->create(['feature_id' => $feature->getId(),'team_id' => $team->getId()]);\n\n# hubspot_webhook_metrics\n\nselect * from opportunities where team_id = 2 and crm_provider_id IN ('374720564','14527423589','49908861993','50435771779'); # 1365\nSELECT * FROM opportunity_contacts WHERE opportunity_id = '414';\nSELECT * FROM opportunity_contacts WHERE crm_provider_id = '131501';\nselect * from contacts where id in (414, 464);\n\nselect * from activities where crm_configuration_id = 2;\n\nselect settings from crm_configurations where id = 11;\n\nselect * from teams; # 1, 2\nselect * from users;\nselect * from crm_configurations where id = 39;\nselect * from team_features where team_id = 2;\nselect * from features;\n# SELECT * FROM opportunities WHERE crm_configuration_id = 2\n# order by id desc;\n# and crm_provider_id = '49908861993';\n\n\nselect * from activity_providers where id IN (443, 202, 203, 227);\n\nselect * from activity_imports where id = 795889;\n\nselect c.id, c.provider, c.settings, t.* from teams t join crm_configurations c on t.id = c.team_id\nwhere c.provider = 'hubspot';\n\nselect * from crm_configurations crm JOIN teams t on crm.team_id = t.id\nwhere provider = 'hubspot';\nSELECT * FROM teams WHERE id = 31;\nSELECT * FROM users WHERE id = 257;\nSELECT * FROM opportunities WHERE team_id = 2;\n\nselect * from opportunity_contacts where opportunity_id = 5124;\nselect * from contacts where id IN (3850,3853,3851,4073,4140,4155,4480,4530,4623,5986,513,687,1806,1523,3613)\n\nselect * from activities where crm_configuration_id = 13;\n\nSELECT * FROM activities WHERE uuid_to_bin('826619ce-ec8e-4e59-8467-a01f5f6ad71e') = uuid; # 418141\n\n\nselect id, team_id, crm_provider_id from crm_configurations where provider = 'hubspot' and crm_provider_id IS NOT NULL;\nSELECT * FROM accounts WHERE team_id = 2 and crm_provider_id = '1212213464' order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 and account_id = 5189 order by id desc;\nSELECT * FROM contacts WHERE team_id = 2 order by id desc;\nselect * from opportunity_contacts where contact_id = 6223;\nSELECT * FROM opportunities WHERE team_id = 2 and account_id = 5189 order by id desc;\n\nselect * from crm_profiles where crm_configuration_id = 2;\n\nselect * from activities where account_id = 46;","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"8","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"49","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"33","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Highlighted Error","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
5177859108263031624
|
921758650370274893
|
click
|
accessibility
|
NULL
|
Project: faVsco.js, menu
master, menu
Start Listen Project: faVsco.js, menu
master, menu
Start Listening for PHP Debug Connections
AskJiminnyReportActivityServiceTest
Run 'AskJiminnyReportActivityServiceTest'
Debug 'AskJiminnyReportActivityServiceTest'
More Actions
JetBrains AI
Search Everywhere
IDE and Project Settings
Execute
Explain Plan
Browse Query History
View Parameters
Open Query Execution Settings…
In-Editor Results
Tx: Auto
Cancel Running Statements
Playground
jiminny
Sync Changes
Hide This Notification
Code changed:
Hide
6
1
6
Previous Highlighted Error
Next Highlighted Error
# [PASSWORD_DOTS] HS [PASSWORD_DOTS]
select * from teams where id = 2; # 2
select * from features; # 2
select * from team_features where team_id = 2; # 2
select * from crm_configurations where id = 2; # 2
select * from users where team_id = 2; #
select * from playbooks where team_id = 2; # event 38
select * from playbook_categories where playbook_id = 38; #
SELECT * FROM activities WHERE crm_configuration_id = 2 and crm_provider_id is not null order by id desc;
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
8
49
1
33
1
Previous Highlighted Error
Next Highlighted Error...
|
4847
|
NULL
|
NULL
|
NULL
|