|
6770
|
295
|
0
|
2026-05-08T07:12:39.998303+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224359998_m1.jpg...
|
Firefox
|
Meet - Daily - Platform — Work
|
True
|
meet.google.com/agt-teir-cwt?authuser=lukas.kovali meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
9
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
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.
Pin Steliyan Georgiev to your main screen
Mute Steliyan Georgiev's microphone
More options for Steliyan Georgiev
Steliyan Georgiev
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.
Pin Stefka Stoyanova to your main screen
Mute Stefka Stoyanova's microphone
More options for Stefka Stoyanova
Stefka Stoyanova
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.
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
User profile picture User profile picture 4 others
4 others
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
Others might see more of your background. Click to view your full video.
10:12
AM
Daily - Platform
Daily - Platform
Audio settings
Turn on microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Turn on microphone (⌘ + d)
Galya Dimitrova joined...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Daily - Platform","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":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08944444,"width":0.04097222,"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":"9","depth":22,"bounds":{"left":0.9145833,"top":0.101111114,"width":0.0048611113,"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.08944444,"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.101111114,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":22,"bounds":{"left":0.96666664,"top":0.101111114,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":21,"bounds":{"left":0.96458334,"top":0.090555556,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"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.5798611,"top":0.61,"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.7239583,"top":0.6244444,"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.7017361,"top":0.6205556,"width":0.11076389,"height":0.05666667},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unpin Nikolay Yankov's presentation from your main screen","depth":13,"bounds":{"left":0.34618056,"top":0.5088889,"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":"AXButton","text":"You can't unmute someone else's presentation","depth":13,"bounds":{"left":0.37395832,"top":0.50666666,"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":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.4045139,"top":0.5088889,"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":"AXButton","text":"Zoom in","depth":13,"bounds":{"left":0.63090277,"top":0.78333336,"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},{"role":"AXButton","text":"Open in new window","depth":13,"bounds":{"left":0.6642361,"top":0.78333336,"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},{"role":"AXButton","text":"Enter Full Screen","depth":13,"bounds":{"left":0.69756943,"top":0.78333336,"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},{"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.78541666,"top":0.27611113,"width":0.14652778,"height":0.07722222},"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.9295139,"top":0.2911111,"width":0.07048613,"height":0.017777778},"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.90729165,"top":0.28666666,"width":0.09270835,"height":0.045},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Steliyan Georgiev to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.25111112,"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":"AXButton","text":"Mute Steliyan Georgiev's microphone","depth":13,"bounds":{"left":0.7885417,"top":0.2488889,"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 Steliyan Georgiev","depth":13,"bounds":{"left":0.8190972,"top":0.25111112,"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":"Steliyan Georgiev","depth":17,"bounds":{"left":0.753125,"top":0.36277777,"width":0.090625,"height":0.022777777},"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.90902776,"top":0.27611113,"width":0.090972245,"height":0.07722222},"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":1.0,"top":0.2911111,"width":-0.053125024,"height":0.017777778},"on_screen":false,"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":1.0,"top":0.28666666,"width":-0.030902743,"height":0.045},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Stefka Stoyanova to your main screen","depth":13,"bounds":{"left":0.8871528,"top":0.25111112,"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":"AXButton","text":"Mute Stefka Stoyanova's microphone","depth":13,"bounds":{"left":0.9149306,"top":0.2488889,"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 Stefka Stoyanova","depth":13,"bounds":{"left":0.9454861,"top":0.25111112,"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":"Stefka Stoyanova","depth":17,"bounds":{"left":0.87951386,"top":0.36277777,"width":0.088194445,"height":0.022777777},"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.78541666,"top":0.5338889,"width":0.14652778,"height":0.07722222},"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.9295139,"top":0.54888886,"width":0.07048613,"height":0.017777778},"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.90729165,"top":0.54444444,"width":0.09270835,"height":0.045},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.5088889,"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":"AXButton","text":"Mute Nikolay Yankov's microphone","depth":13,"bounds":{"left":0.7885417,"top":0.50666666,"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 Nikolay Yankov","depth":13,"bounds":{"left":0.8190972,"top":0.5088889,"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":"Nikolay Yankov","depth":17,"bounds":{"left":0.753125,"top":0.6205556,"width":0.07673611,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"User profile picture User profile picture 4 others","depth":11,"bounds":{"left":0.87118053,"top":0.40888888,"width":0.11805555,"height":0.24444444},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 others","depth":13,"bounds":{"left":0.90902776,"top":0.55722225,"width":0.04236111,"height":0.022777777},"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.8840278,"top":0.7916667,"width":0.11597222,"height":0.07722222},"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.8107639,"top":0.8066667,"width":0.07569444,"height":0.017777778},"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.79131943,"top":0.80222225,"width":0.11736111,"height":0.045},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"bounds":{"left":0.82256943,"top":0.7644445,"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.853125,"top":0.7644445,"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.8836806,"top":0.76666665,"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.753125,"top":0.87833333,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"bounds":{"left":0.96631944,"top":0.875,"width":0.018055556,"height":0.028888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"10:12","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.02638889,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"AM","depth":12,"bounds":{"left":0.08020833,"top":0.9444444,"width":0.017708333,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Daily - Platform","depth":12,"bounds":{"left":0.115277775,"top":0.9111111,"width":0.08090278,"height":0.08888888},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Daily - Platform","depth":15,"bounds":{"left":0.115277775,"top":0.9444444,"width":0.08090278,"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":"Nikolay Yankov is presenting","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 on microphone (⌘ + d)","depth":10,"bounds":{"left":0.3125,"top":0.9027778,"width":0.105902776,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Galya Dimitrova joined","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
6536102339872649976
|
-6570218328574723300
|
idle
|
accessibility
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
9
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
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.
Pin Steliyan Georgiev to your main screen
Mute Steliyan Georgiev's microphone
More options for Steliyan Georgiev
Steliyan Georgiev
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.
Pin Stefka Stoyanova to your main screen
Mute Stefka Stoyanova's microphone
More options for Stefka Stoyanova
Stefka Stoyanova
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.
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
User profile picture User profile picture 4 others
4 others
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
Others might see more of your background. Click to view your full video.
10:12
AM
Daily - Platform
Daily - Platform
Audio settings
Turn on microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Turn on microphone (⌘ + d)
Galya Dimitrova joined...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
6764
|
294
|
10
|
2026-05-08T07:11:32.315012+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224292315_m2.jpg...
|
Firefox
|
Meet - Daily - Platform — Work
|
True
|
meet.google.com/agt-teir-cwt?authuser=lukas.kovali meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_2
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
9
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
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.
Pin Steliyan Georgiev to your main screen
Mute Steliyan Georgiev's microphone
More options for Steliyan Georgiev
Steliyan Georgiev
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.
Pin Stefka Stoyanova to your main screen
Mute Stefka Stoyanova's microphone
More options for Stefka Stoyanova
Stefka Stoyanova
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.
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
User profile picture User profile picture 4 others
4 others
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
Others might see more of your background. Click to view your full video.
10:11
AM
Daily - Platform
Daily - Platform
Audio settings
Turn on microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Galya Dimitrova joined...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Daily - Platform","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":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.30634972,"top":1.0,"width":0.059507977,"height":-0.072625756},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.30634972,"top":1.0,"width":0.059507977,"height":-0.07342374},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.69481385,"top":1.0,"width":0.019614361,"height":-0.06424582},"on_screen":true,"role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"9","depth":22,"bounds":{"left":0.7081117,"top":1.0,"width":0.0023271276,"height":-0.072625756},"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.06424582},"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.072625756},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":22,"bounds":{"left":0.7330452,"top":1.0,"width":0.013464096,"height":-0.072625756},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":21,"bounds":{"left":0.73204786,"top":1.0,"width":0.011303191,"height":-0.065043926},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"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":"Unpin Nikolay Yankov's presentation from your main screen","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":"You can't unmute someone else's presentation","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":false,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"More options for Nikolay Yankov","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":"Zoom in","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Open in new window","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXButton","text":"Enter Full Screen","depth":13,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"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":"Pin Steliyan Georgiev to your main screen","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":"Mute Steliyan Georgiev's 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":"More options for Steliyan Georgiev","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":"Steliyan Georgiev","depth":17,"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":false,"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":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Stefka Stoyanova to your main screen","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":"Mute Stefka Stoyanova's 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":"More options for Stefka Stoyanova","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":"Stefka Stoyanova","depth":17,"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":"Pin Nikolay Yankov to your main screen","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":"Mute Nikolay Yankov's 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":"More options for Nikolay Yankov","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":"Nikolay Yankov","depth":17,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"User profile picture User profile picture 4 others","depth":11,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 others","depth":13,"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":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"10:11","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"AM","depth":12,"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Daily - Platform","depth":12,"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Daily - Platform","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":"Nikolay Yankov is presenting","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":"Galya Dimitrova joined","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
3163854137734886094
|
-6570218328575247588
|
click
|
hybrid
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
9
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
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.
Pin Steliyan Georgiev to your main screen
Mute Steliyan Georgiev's microphone
More options for Steliyan Georgiev
Steliyan Georgiev
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.
Pin Stefka Stoyanova to your main screen
Mute Stefka Stoyanova's microphone
More options for Stefka Stoyanova
Stefka Stoyanova
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.
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
User profile picture User profile picture 4 others
4 others
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
Others might see more of your background. Click to view your full video.
10:11
AM
Daily - Platform
Daily - Platform
Audio settings
Turn on microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Galya Dimitrova joined
PhostormINavicatecodeFV faVsco.js°9 master kProledeyD DeleteCrmEntityTrait.php(©) CrmActivityService.phg© HubspotWebhoc• спескалакешукеmоteмateh.php© JiminnyDebugCommand.phpv @ Pagination© RateLimitException.php© PaginationConfi‹ © ResponseException.php© PaginationState.phg©)Paginationconrig.pngC) Paginationstate.#Badkequest.ono© OpportunitySyncTrait.phpTImportBatchJobTrait.phg© SyncHubspotObiects.phd•_ Prospectsearchstr> D Redis(C) HydrateCrmDataByExternalCallidJob.php© ConferenceCrmMatcherJob.phpC) MatchCrmData.php(C) Activity.phov D ServiceTraitsC) DeraultUpdateCrmDataResolver.phpC) CachedCrmServiceDecorator.pho0 Servicelntertace.php+ Opportunitvsyn+ SyncermEntitiessendnointCc W .*I 7:T SuncFieldstrait.class huospocraoinaclonserviceT Writecrmtrait.ol•DUts• WeonookC) BatchSvncCollectot 1621C) BatchSvncRedisSerlC) Client.ohoC) ClosedDealStadesS 164C DealFieldsService.p 165© DecorateActivity.pt 166(C) SioldDefinitions nhr 149(C) SieldTvneConvertel 149^ HubsnotClientinten 195© HubspotTokenMan: 196© PayloadBuilder.php 197© RemoteCrmObjectN 198© ResponseNormalize 199c) service.ono© SyncFieldAction.ph© SyncRelatedActivits 20гc) WebhooksyncBatci 207C IntegrationApp› Accessors• W Api|• contioMDTO•D Filtersaobs• ProspectSearchStr: 214v ServiceTraitsTSyternalManstirs 219tinternalAccounts 210? LavoutTrait.nhn 22d4T MatchProsnects 22117. NotSunnortedTr 229? SvncCrmEntities 227)SvncCrmSieldsT 221T CuncCrmMotade 2n6T SystemStateTra(e) DataClient.php© DecorateActivity.pr 236private function validateTokenIfNeeded(Client Sclient, PaginationState $state): voidt...}private function executeSearchRequest(Client Sclient, string $objectType, array Spayload, PaginationState $stat«*tryreturn $client->search(SobjectType, Spayload);} catch (\Exception $e) {if (Sclient->isUnauthorizedException(Se)) {...}I/ RateLimitException and other exceptions are re-thrown as-isthrow seprivate function updatelastRecordId(array Spage. PaginationState Sstate): void{...}orivate function getnext0ffsetarrav Soage): int-...-private function logPaginationProgress(PaginationState $state, int $teamId, string $et• voridifSstate->chouldLoaProanessobSthis->logger->info('[Hubspot) Pagination progress log', [Iteam id' => SteamTd.londnointt =s SeInoquocte madol => Cctato-snonuoctCount'records_fetched' => $state->totalRecords,'elapsed seconds' => $state->getElapsedSeconds@.private function calculateDelavInMicroseconds@: intf...}Sonar@uhe forlidElsuaaestiions. Detect.more secmitvlicauesfin.vour D.D/files //liaSonarOube Claudfor.free//lDownload.SonarOnbe.Server//llearn.more//lDonit ask again/vesterdav 10:25© ProviderRateLimiter.php X = custom.logElaravel.log4 SF jiminny@localhost]A HS_local [jiminny@localhost]tiò accounts fimA console (PROD]# console [eu)A console [STAGING]use...class Providerratelimitenprotected Ratelimiter SrateLimiter:public function __construct(RateLimiter $rateLimiter)(...}public function canMakeRequest(RateLimited Sprovider): bool/** Ovar RateLimitInterface $rateLimit */foreach (Sprovider->getRateLimits as $rateLimit) ‹$key = SrateLimit->getKeyO;if (Sthis->rateLimiter->tooManyAttempts($key, SrateLimit->getQuotaO)) ‹return true:pubLic tunction requestavallableln kateLimited Sprovider: 1ntreturn Sprovider->getRateLimits(->isNotEmptv@Sproviden->getRateLimits@(RateLimitInterface SrateLimit): int => Sthis->rateLimiter->availableIn(SrateLii->maxonublic function incrementReauestCountRateLimited Sorovider)• void** Avan Ratel imitIntenface Sratel imit *^foreach (Sprovider->getRateLimits as $rateLimit) ‹Sthic-snatoimit->getKey, SrateLimit->getWindowO):ont Dally • In 41 491100% 12Fri 8 May 10:11:32AskJiminnyReportActivityServiceTest vCascadeHubspot Rate LimitingHubSpot Rate Limit H+0 ..4-9 requests per second on search enapoints•Policy name: SECONDLY• Retry-After: 1 second•Applies to: search endpoints only (/crm/v3/objects/type/search )The middleware handles both via narseRetrvAfter() lin Client.0ho:125-171.Part 3: 1300 Search Calls — What Actualllv Haopens?Math Setup•1300 search calls needed• Search limit: 4-5 realsec = ~250-300 ner minute max• Minimum total time: 1300 / 5 = 260 seconds (~4.3 minutes) if perfectly pacedScenario A: WITHOUT MiddlewareTEUS:06 noos picked up oy workers tvoica concurrency.T=0-1g.~5 succeed (within search rate limit)~95 hit SECONDLY (1s) exception bubbles up → J0B FAILST=1s:Failed jobs released with backoff[0]=30s95 1o0s retry simultaneousiy → ALlnat SEcoNdLy again - FAlL agarnT-20cReleaced with hackoffl11-00cT=309s.again - SECONDLY hit - FAIL - backoff[2]=180sFinal retrv - likelv FATL again - pERMANENT FATLUREMeanwhile, the cumulative volume hits TEN SECONDLY ROLLING:- 100÷ requests in 10c window from retries - 420 with nolicvlame-TFN SFCONDIY ROLITNGFinal outcome:- ~5-10% of jobs succeed organically- 2009 hit <triec-2 limit ~ PERMANENT FATIURE- Sentry: ~3900+ error events (1300 x 3 attempts)Scenario 8: WiTH Middleware100 jobs picked upT=0-1s: 5 succeed; 95 hit SECONDLY (retryAfter=1)- middleware catches → release@)- attemot count NOT incremented* Reiect allAccent alliAsk anvthina (&4D)Claude Onus 4.7 MediumWN Windsurf Toams 166.22UTF.8io 4 spaces...
|
6763
|
NULL
|
NULL
|
NULL
|
|
6763
|
294
|
9
|
2026-05-08T07:10:51.471501+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224251471_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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Show Replace Field
Search History
$endpoint
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/5
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
13
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $type, $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 $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} 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 {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} 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;
}
}
// RateLimitException and other exceptions are re-thrown as-is
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"bounds":{"left":0.10472074,"top":0.22905028,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"bounds":{"left":0.11735372,"top":0.22825219,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"$endpoint","depth":4,"bounds":{"left":0.12832446,"top":0.22825219,"width":0.043882977,"height":0.015961692},"on_screen":true,"value":"$endpoint","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.18118352,"top":0.22825219,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"bounds":{"left":0.19115691,"top":0.22825219,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"bounds":{"left":0.19980054,"top":0.22825219,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"bounds":{"left":0.20844415,"top":0.22825219,"width":0.00731383,"height":0.017557861},"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.27027926,"top":1.0,"width":0.00731383,"height":0.0},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/5","depth":4,"bounds":{"left":0.22207446,"top":0.22745411,"width":0.025598405,"height":0.017557861},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"bounds":{"left":0.24767287,"top":0.22665602,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"bounds":{"left":0.25631648,"top":0.22665602,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"bounds":{"left":0.2649601,"top":0.22665602,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"bounds":{"left":0.27360374,"top":0.22665602,"width":0.008643617,"height":0.01915403},"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"bounds":{"left":0.40791222,"top":0.22665602,"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":"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":"13","depth":4,"bounds":{"left":0.39527926,"top":0.25778133,"width":0.009640957,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.25618514,"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.41389626,"top":0.25618514,"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, $type, $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 $objectType, array $payload, PaginationState $state): array\n {\n try {\n return $client->search($objectType, $payload);\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 $result = $client->search($objectType, $payload);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $result;\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 }\n\n // RateLimitException and other exceptions are re-thrown as-is\n throw $e;\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,"bounds":{"left":0.122340426,"top":0.0,"width":0.39960107,"height":1.0},"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, $type, $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 $objectType, array $payload, PaginationState $state): array\n {\n try {\n return $client->search($objectType, $payload);\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 $result = $client->search($objectType, $payload);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $result;\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 }\n\n // RateLimitException and other exceptions are re-thrown as-is\n throw $e;\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}]...
|
3401949268335374344
|
-2274930371109148689
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Show Replace Field
Search History
$endpoint
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/5
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
13
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $type, $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 $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} 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 {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} 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;
}
}
// RateLimitException and other exceptions are re-thrown as-is
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...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
6761
|
294
|
8
|
2026-05-08T07:10:46.149126+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224246149_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
Sync Changes...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"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}]...
|
6716744925979352142
|
-8204420343923495994
|
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
Sync Changes
PhostormINavigarecodeFV faVsco.jsProiect© BatchSyncCollectol© JiminnyDebugCommand.phpe balchsynckealsse© ClosedDealStagesS © ResponseException.phgPaginationeontig.ongDealrielasservice.gc)Decorateacuiviy.or© FieldDefinitions.phrC) FieldT vpeconvertee Hubspotclientinterc) Hubspotlokenman© PayloadBuilder.phpC) RemotecrmobiectrP ResponseNormalizec) Service,ono© SyncFieldAction.phC) SvncRelatedActivitC) WebhookSvncBatclv MintearationAorM Acceccors• D ConfigD DT• M SiltersD Jobs. M ProspectSearchStrW service lraitsT ExternalMapsTr.a Interna Account© LayoutTrait.php(a) Match Dracnonte* NotSupportedTr© SyncCrmEntities© SyncCrmFieldsTw syncermmetadaTsvstemstateliraC) DataClient.ohoC)DecorateActivitv.ohC)LocalSearch.onv1) LocalSearchinterfacC) RemoteSearch.ohoC) Service.ohnv M listenersC) Convertl eadActivit(C) Purael ookunCache> M Metadata> D MigrationM Pinedrive> @ OpportunitySyncSt)m DrocnontConrchCtr© ApiFields.php(e) Client nhr#Badkequest.onoc [EMAIL]) HydrateCrmDataByExternalCallidJob.phpC) MatchCrmData.php© Activity.phcC) DeraultUpdateCrmDataResolver.phpC) CachedCrmServiceDecorator.pho0 Servicelntertace.phpclass Cuient extends BasecLient imolements HubspotcuientinterfaceA2 A69 V.2 ^224226 C239243250function getPaginatedData(array Spayload, string $type, int $offset = 0): arrayreturn 'results' => Srows."total' => Stotal.'last record' => SlastIdl* athrows HubsnotExcent.ion*athrows SocialAccountTokeninvalid=xcent.ion* athrows BadReauestpublic function getPaginatedDataGeneratorddi'l'dy sodycodd,string Stypeint Soffset = 0,int &$total = 0,?string &$lastRecordId = null): \Generator {return Sthis->paginationService->getPaqinatedDataGenerator(Scnis,SpayloadStype,: Stotal: SlastRecordiid* Execute a search reauest agdinst HubSoot CRM obiects with rate Limitina.* @param string Sobjecttype The object type ('deals', 'companies', 'contacts', 'calls')* Eparam array<string, mixed› $payload The search payload with filters, sorts, properties, etc.* Areturn arrau The search resnonse withiItotaltInaginal keus* Othrows Ratel imitFxcention When nate limit is hit.Othrows HubsnotFycention On APT erronçlpublic function search(string SobjectType, array Spayload): array$endpoint = self::BASE_URL . "/crm/v3/objects/{$objectType}/search":arQube for INF suadections. Netect more cecurity iccuec in vour DHP filec II Try So.arQube Cloud for fres /I Download SonarQuhe Sorver |l I earn more I Don't ack adain /vecterdav 10-25© ProviderRateLimiter.php X = custom.logElaravel.log4 SF jiminny@localhost]A HS_local [jiminny@localhost]tiò accounts fürA console (PROD]# console [eu)A console [STAGING]use...class Providerratelimitenorotected RateLimiter SrateLimiter:public function __construct(RateLimiter $rateLimiter)(...}public function canMakeRequest(RateLimited $provider): bool/** Ovar RateLimitInterface $rateLimit */foreach (Sprovider->getRateLimits as $rateLimit){$key = SrateLimit->getKeyO;if (Sthis->rateLimiter->tooManyAttempts($key, SrateLimit->getQuotaO)) ‹ДДДАpubLic tunction requestava1lableln kateLimited Sprovider: 1ntreturn Sprovider->getRateLimits@->isNotEmptv@Sproviden->getRateLimits@(RateLimitInterface SrateLimit): int => Sthis->rateLimiter->availableIn(SrateLii->maxonublic function incnementReauestCountRateLimited Sorovider)• voidl** Avan Ratel imitIntenface Sratel imit *^foreach (Sprovider->getRateLimits) as SrateLimit){mit-›getKey, $rateLimit->getWindow0):suppont Dally • In 4h o0m100% 12?.Fri 8 May 10:10:47U AskJiminnyReportActivityServiceTest vCascadeHubspot Rate LimitingHubSpot Rate Limit H+0 ..4-9 requests per second on search enapoints•Policy name: SECONDLY• Retry-After: 1 secondApplies to: search endpoints only /crm/v3/objects/type/search ))The middleware handles both via narseRetrvAfter() lin Client.0ho: 125-171.Part 3: 1300 Search Calls — What Actualllv Haopens?Math Setup•1300 search calls needed• Search limit: 4-5 realsec = ~250-300 ner minute max• Minimum total time: 1300 / 5 = 260 seconds (~4.3 minutes) if perfectly pacedScenario A: WITHOUT MiddlewareTEUS:06 noos picked up oy workers tvoical concurrency.T=0-1s.~5 succeed (within search rate limit)~95 hit SECONDLY (1s) exception bubbles up → J0B FAILST=1s:Failed jobs released with backoff[0]=30s95 1o0s retry simultaneousiy → ALlnat SEcoNdLy again - FAlL agarnT-20cReleaced with hackoffl11-00cT=309s.again - SECONDLY hit - FAIL - backoff[2]=180sFinal retrv - likelv FATL again - pERMANENT FATLUREMeanwhile, the cumulative volume hits TEN SECONDLY ROLLING:- 100÷ requests in 19c window from retriec - 420 with nolicvlame-TFN SFCONDIY ROLLTNGFinal outcome:- ~5-10% of jobs succeed organically- 2009 hit <triec-2 limit ~ PERMANENT FATIURE- Sentry: ~3900+ error events (1300 x 3 attempts)Scenario 8: WiTH Middleware100 jobs picked upT=0-1s: 5 succeed; 95 hit SECONDLY (retryAfter=1)- middleware catches → release@)- attemot count NOT incremented* Reiect allAccent alliAsk anvthina (&4D)Claude Onus 4.7 MediumWN Windsurf Toams 222•61UTF.8io 4 spaces...
|
6759
|
NULL
|
NULL
|
NULL
|
|
6759
|
294
|
7
|
2026-05-08T07:10:43.218529+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224243218_m2.jpg...
|
PhpStorm
|
faVsco.js – Client.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}...
|
[{"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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6322393822977483
|
-2563669779787567664
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
6756
|
294
|
6
|
2026-05-08T07:10:25.171796+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224225171_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error...
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"bounds":{"left":0.025930852,"top":0.019952115,"width":0.03856383,"height":0.025538707},"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"width":0.006981383,"height":0.018355945},"on_screen":true,"role_description":"button","is_enabled":true,"is_selected":false,"is_expanded":false}]...
|
6857581337665978828
|
-3714339759526801968
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error...
|
6748
|
NULL
|
NULL
|
NULL
|
|
6754
|
294
|
5
|
2026-05-08T07:09:52.514943+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224192514_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6748
|
NULL
|
NULL
|
NULL
|
|
6752
|
294
|
4
|
2026-05-08T07:09:21.855741+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224161855_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6748
|
NULL
|
NULL
|
NULL
|
|
6750
|
294
|
3
|
2026-05-08T07:08:51.214594+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224131214_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6748
|
NULL
|
NULL
|
NULL
|
|
6748
|
294
|
2
|
2026-05-08T07:08:20.447206+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224100447_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
6746
|
294
|
1
|
2026-05-08T07:07:49.518916+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224069518_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}...
|
[{"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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
-6322393822977483
|
-2563669779787567664
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6745
|
294
|
0
|
2026-05-08T07:07:47.853211+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224067853_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6766
|
293
|
10
|
2026-05-08T07:11:38.487081+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224298487_m1.jpg...
|
Firefox
|
Meet - Daily - Platform — Work
|
True
|
meet.google.com/agt-teir-cwt?authuser=lukas.kovali meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
9
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
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.
Pin Steliyan Georgiev to your main screen
Mute Steliyan Georgiev's microphone
More options for Steliyan Georgiev
Steliyan Georgiev
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.
Pin Stefka Stoyanova to your main screen
Mute Stefka Stoyanova's microphone
More options for Stefka Stoyanova
Stefka Stoyanova
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.
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
User profile picture User profile picture 4 others
4 others
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
Others might see more of your background. Click to view your full video.
10:11
AM
Daily - Platform
Daily - Platform
Audio settings
Turn on microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Turn on microphone (⌘ + d)
Galya Dimitrova joined...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Daily - Platform","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":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08944444,"width":0.04097222,"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":"9","depth":22,"bounds":{"left":0.9145833,"top":0.101111114,"width":0.0048611113,"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.08944444,"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.101111114,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":22,"bounds":{"left":0.96666664,"top":0.101111114,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":21,"bounds":{"left":0.96458334,"top":0.090555556,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"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.5798611,"top":0.61,"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.7239583,"top":0.6244444,"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.7017361,"top":0.6205556,"width":0.11076389,"height":0.05666667},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unpin Nikolay Yankov's presentation from your main screen","depth":13,"bounds":{"left":0.34618056,"top":0.5088889,"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":"AXButton","text":"You can't unmute someone else's presentation","depth":13,"bounds":{"left":0.37395832,"top":0.50666666,"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":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.4045139,"top":0.5088889,"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":"AXButton","text":"Zoom in","depth":13,"bounds":{"left":0.63090277,"top":0.78333336,"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},{"role":"AXButton","text":"Open in new window","depth":13,"bounds":{"left":0.6642361,"top":0.78333336,"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},{"role":"AXButton","text":"Enter Full Screen","depth":13,"bounds":{"left":0.69756943,"top":0.78333336,"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},{"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.78541666,"top":0.27611113,"width":0.14652778,"height":0.07722222},"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.9295139,"top":0.2911111,"width":0.07048613,"height":0.017777778},"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.90729165,"top":0.28666666,"width":0.09270835,"height":0.045},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Steliyan Georgiev to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.25111112,"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":"AXButton","text":"Mute Steliyan Georgiev's microphone","depth":13,"bounds":{"left":0.7885417,"top":0.2488889,"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 Steliyan Georgiev","depth":13,"bounds":{"left":0.8190972,"top":0.25111112,"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":"Steliyan Georgiev","depth":17,"bounds":{"left":0.753125,"top":0.36277777,"width":0.090625,"height":0.022777777},"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.90902776,"top":0.27611113,"width":0.090972245,"height":0.07722222},"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":1.0,"top":0.2911111,"width":-0.053125024,"height":0.017777778},"on_screen":false,"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":1.0,"top":0.28666666,"width":-0.030902743,"height":0.045},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Stefka Stoyanova to your main screen","depth":13,"bounds":{"left":0.8871528,"top":0.25111112,"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":"AXButton","text":"Mute Stefka Stoyanova's microphone","depth":13,"bounds":{"left":0.9149306,"top":0.2488889,"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 Stefka Stoyanova","depth":13,"bounds":{"left":0.9454861,"top":0.25111112,"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":"Stefka Stoyanova","depth":17,"bounds":{"left":0.87951386,"top":0.36277777,"width":0.088194445,"height":0.022777777},"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.78541666,"top":0.5338889,"width":0.14652778,"height":0.07722222},"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.9295139,"top":0.54888886,"width":0.07048613,"height":0.017777778},"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.90729165,"top":0.54444444,"width":0.09270835,"height":0.045},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.5088889,"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":"AXButton","text":"Mute Nikolay Yankov's microphone","depth":13,"bounds":{"left":0.7885417,"top":0.50666666,"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 Nikolay Yankov","depth":13,"bounds":{"left":0.8190972,"top":0.5088889,"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":"Nikolay Yankov","depth":17,"bounds":{"left":0.753125,"top":0.6205556,"width":0.07673611,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"User profile picture User profile picture 4 others","depth":11,"bounds":{"left":0.87118053,"top":0.40888888,"width":0.11805555,"height":0.24444444},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 others","depth":13,"bounds":{"left":0.90902776,"top":0.55722225,"width":0.04236111,"height":0.022777777},"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.8840278,"top":0.7916667,"width":0.11597222,"height":0.07722222},"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.8107639,"top":0.8066667,"width":0.07569444,"height":0.017777778},"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.79131943,"top":0.80222225,"width":0.11736111,"height":0.045},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"bounds":{"left":0.82256943,"top":0.7644445,"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.853125,"top":0.7644445,"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.8836806,"top":0.76666665,"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.753125,"top":0.87833333,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"bounds":{"left":0.96631944,"top":0.875,"width":0.018055556,"height":0.028888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"10:11","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.025347222,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"AM","depth":12,"bounds":{"left":0.079166666,"top":0.9444444,"width":0.017708333,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Daily - Platform","depth":12,"bounds":{"left":0.11423611,"top":0.9111111,"width":0.08090278,"height":0.08888888},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Daily - Platform","depth":15,"bounds":{"left":0.11423611,"top":0.9444444,"width":0.08090278,"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":"Nikolay Yankov is presenting","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 on microphone (⌘ + d)","depth":10,"bounds":{"left":0.3125,"top":0.9027778,"width":0.105902776,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Galya Dimitrova joined","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
-1965493713997687649
|
-6570218328574723300
|
visual_change
|
accessibility
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
9
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
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.
Pin Steliyan Georgiev to your main screen
Mute Steliyan Georgiev's microphone
More options for Steliyan Georgiev
Steliyan Georgiev
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.
Pin Stefka Stoyanova to your main screen
Mute Stefka Stoyanova's microphone
More options for Stefka Stoyanova
Stefka Stoyanova
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.
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
User profile picture User profile picture 4 others
4 others
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
Others might see more of your background. Click to view your full video.
10:11
AM
Daily - Platform
Daily - Platform
Audio settings
Turn on microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Turn on microphone (⌘ + d)
Galya Dimitrova joined...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
6765
|
293
|
9
|
2026-05-08T07:11:32.736076+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224292736_m1.jpg...
|
Firefox
|
Meet - Daily - Platform — Work
|
True
|
meet.google.com/agt-teir-cwt?authuser=lukas.kovali meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.com...
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
9
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
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.
Pin Steliyan Georgiev to your main screen
Mute Steliyan Georgiev's microphone
More options for Steliyan Georgiev
Steliyan Georgiev
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.
Pin Stefka Stoyanova to your main screen
Mute Stefka Stoyanova's microphone
More options for Stefka Stoyanova
Stefka Stoyanova
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.
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
User profile picture User profile picture 4 others
4 others
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
Others might see more of your background. Click to view your full video.
10:11
AM
Daily - Platform
Daily - Platform
Audio settings
Turn on microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Galya Dimitrova joined...
|
[{"role":"AXRadioButton","text [{"role":"AXRadioButton","text":"Meet - Daily - Platform","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":"AXHeading","text":"Nikolay Yankov (Presenting)","depth":12,"bounds":{"left":0.07534722,"top":0.101111114,"width":0.124305554,"height":0.022222223},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Nikolay Yankov (Presenting)","depth":13,"bounds":{"left":0.07534722,"top":0.10222222,"width":0.124305554,"height":0.020555556},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"People","depth":15,"bounds":{"left":0.88680553,"top":0.08944444,"width":0.04097222,"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":"9","depth":22,"bounds":{"left":0.9145833,"top":0.101111114,"width":0.0048611113,"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.08944444,"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.101111114,"width":0.06388891,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Gemini","depth":22,"bounds":{"left":0.96666664,"top":0.101111114,"width":0.028125,"height":0.017222222},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Gemini","depth":21,"bounds":{"left":0.96458334,"top":0.090555556,"width":0.023611112,"height":0.037777778},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"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.5798611,"top":0.61,"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.7239583,"top":0.6244444,"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.7017361,"top":0.6205556,"width":0.11076389,"height":0.05666667},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Unpin Nikolay Yankov's presentation from your main screen","depth":13,"bounds":{"left":0.34618056,"top":0.5088889,"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":"AXButton","text":"You can't unmute someone else's presentation","depth":13,"bounds":{"left":0.37395832,"top":0.50666666,"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":"More options for Nikolay Yankov","depth":13,"bounds":{"left":0.4045139,"top":0.5088889,"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":"AXButton","text":"Zoom in","depth":13,"bounds":{"left":0.63090277,"top":0.78333336,"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},{"role":"AXButton","text":"Open in new window","depth":13,"bounds":{"left":0.6642361,"top":0.78333336,"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},{"role":"AXButton","text":"Enter Full Screen","depth":13,"bounds":{"left":0.69756943,"top":0.78333336,"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},{"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.78541666,"top":0.27611113,"width":0.14652778,"height":0.07722222},"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.9295139,"top":0.2911111,"width":0.07048613,"height":0.017777778},"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.90729165,"top":0.28666666,"width":0.09270835,"height":0.045},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Steliyan Georgiev to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.25111112,"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":"AXButton","text":"Mute Steliyan Georgiev's microphone","depth":13,"bounds":{"left":0.7885417,"top":0.2488889,"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 Steliyan Georgiev","depth":13,"bounds":{"left":0.8190972,"top":0.25111112,"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":"Steliyan Georgiev","depth":17,"bounds":{"left":0.753125,"top":0.36277777,"width":0.090625,"height":0.022777777},"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.90902776,"top":0.27611113,"width":0.090972245,"height":0.07722222},"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":1.0,"top":0.2911111,"width":-0.053125024,"height":0.017777778},"on_screen":false,"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":1.0,"top":0.28666666,"width":-0.030902743,"height":0.045},"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Stefka Stoyanova to your main screen","depth":13,"bounds":{"left":0.8871528,"top":0.25111112,"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":"AXButton","text":"Mute Stefka Stoyanova's microphone","depth":13,"bounds":{"left":0.9149306,"top":0.2488889,"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 Stefka Stoyanova","depth":13,"bounds":{"left":0.9454861,"top":0.25111112,"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":"Stefka Stoyanova","depth":17,"bounds":{"left":0.87951386,"top":0.36277777,"width":0.088194445,"height":0.022777777},"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.78541666,"top":0.5338889,"width":0.14652778,"height":0.07722222},"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.9295139,"top":0.54888886,"width":0.07048613,"height":0.017777778},"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.90729165,"top":0.54444444,"width":0.09270835,"height":0.045},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Pin Nikolay Yankov to your main screen","depth":13,"bounds":{"left":0.7607639,"top":0.5088889,"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":"AXButton","text":"Mute Nikolay Yankov's microphone","depth":13,"bounds":{"left":0.7885417,"top":0.50666666,"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 Nikolay Yankov","depth":13,"bounds":{"left":0.8190972,"top":0.5088889,"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":"Nikolay Yankov","depth":17,"bounds":{"left":0.753125,"top":0.6205556,"width":0.07673611,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"User profile picture User profile picture 4 others","depth":11,"bounds":{"left":0.87118053,"top":0.40888888,"width":0.11805555,"height":0.24444444},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"4 others","depth":13,"bounds":{"left":0.90902776,"top":0.55722225,"width":0.04236111,"height":0.022777777},"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.8840278,"top":0.7916667,"width":0.11597222,"height":0.07722222},"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.8107639,"top":0.8066667,"width":0.07569444,"height":0.017777778},"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.79131943,"top":0.80222225,"width":0.11736111,"height":0.045},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"You’re continuously framed","depth":13,"bounds":{"left":0.82256943,"top":0.7644445,"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.853125,"top":0.7644445,"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.8836806,"top":0.76666665,"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.753125,"top":0.87833333,"width":0.06875,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXButton","text":"Others might see more of your background. Click to view your full video.","depth":14,"bounds":{"left":0.96631944,"top":0.875,"width":0.018055556,"height":0.028888889},"on_screen":true,"help_text":"","role_description":"button","subrole":"AXUnknown","is_enabled":true,"is_focused":false,"is_selected":false},{"role":"AXStaticText","text":"10:11","depth":12,"bounds":{"left":0.050347224,"top":0.9444444,"width":0.025347222,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXStaticText","text":"AM","depth":12,"bounds":{"left":0.079166666,"top":0.9444444,"width":0.017708333,"height":0.022777777},"on_screen":true,"help_text":"","role_description":"text","subrole":"AXUnknown"},{"role":"AXHeading","text":"Daily - Platform","depth":12,"bounds":{"left":0.11423611,"top":0.9111111,"width":0.08090278,"height":0.08888888},"on_screen":true,"help_text":"","role_description":"heading","subrole":"AXUnknown"},{"role":"AXStaticText","text":"Daily - Platform","depth":15,"bounds":{"left":0.11423611,"top":0.9444444,"width":0.08090278,"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":"Nikolay Yankov is presenting","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":"Galya Dimitrova joined","depth":8,"on_screen":false,"help_text":"","role_description":"text","subrole":"AXUnknown"}]...
|
3163854137734886094
|
-6570218328575247588
|
click
|
hybrid
|
NULL
|
Meet - Daily - Platform
Close tab
New Tab
Open Goo Meet - Daily - Platform
Close tab
New Tab
Open Google Gemini (⌃X)
Tabs from other devices
Open history (⇧⌘H)
Open bookmarks (⌘B)
Customize sidebar
Nikolay Yankov (Presenting)
Nikolay Yankov (Presenting)
People
9
Take notes with Gemini
Take notes with Gemini
Gemini
Gemini
Pop out this video More screens are more fun. Play this video while you do other things.
Pop out this video
More screens are more fun. Play this video while you do other things.
Unpin Nikolay Yankov's presentation from your main screen
You can't unmute someone else's presentation
More options for Nikolay Yankov
Zoom in
Open in new window
Enter Full Screen
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.
Pin Steliyan Georgiev to your main screen
Mute Steliyan Georgiev's microphone
More options for Steliyan Georgiev
Steliyan Georgiev
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.
Pin Stefka Stoyanova to your main screen
Mute Stefka Stoyanova's microphone
More options for Stefka Stoyanova
Stefka Stoyanova
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.
Pin Nikolay Yankov to your main screen
Mute Nikolay Yankov's microphone
More options for Nikolay Yankov
Nikolay Yankov
User profile picture User profile picture 4 others
4 others
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
Others might see more of your background. Click to view your full video.
10:11
AM
Daily - Platform
Daily - Platform
Audio settings
Turn on microphone
Video settings
Turn off camera
Nikolay Yankov is presenting
Send a reaction
Turn on captions
Raise hand (ctrl + ⌘ + h)
More options
Leave call
Meeting details
Chat with everyone
Meeting tools
Galya Dimitrova joined
+FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelp→Q.meet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)57 ServicePlarha: x | E3 Jmine83 Promo:CalisX MOPIE DatadogPlatform Team %.Q Search boardS009 J1-19S051 1 JT-20301KRADTYOK DYAJ Panorama for Call Scoring in ODIn DevImprove StorySetup test coverage forWaheOnMAINTENANCIDeckoeE JY-199511***=1BACKLOG~ DescriptionCurrently AJ Panorama in OD allows managers to get insights about the performance of their team. We have now added afunctionality for Al Call Scoring that also provides insights about the performance. It wil be good to leverage both of themin the chat so that managers can get the best results.• include the Call Score information for each call in Panorama• the context should include - total score, score card name, individual rules score, scoring criteria (the customer'sprompt for the rule), justification, timestamp. This information is stored in the ESocxoPRODUSTRURMNTXEAUTVURUSYIN OAMEADYFOR OASubtasksAdd subtaskView workfiowAUICTTEyour codein creatingJull requestExplain workfiowLinked work itemsAdd linked work itecActivityHsorWvork. 1oalDevelc omenQ Open with VS CodeIJ Create branch4 Create commitSuggest a replyStatus update...ComponentsPlatformUnenkaseePro tios press M to colSub-ProductlAdd optionsC Dev10:11 AM | Daily - Platform# Support Daily - in 4h 49 m100% [8• Fri 8 May 10:11:3398• Fri8 May 10:11L Al BookmarxsGroup: QueriesAl Reports > Empty pagedesign and promotionAJREPORTSDeployed8 20372 1 0 0000=GOK Va AZUreД JY-20728 1 0 +++=Allow users to deiete SSand Panorama promptswhen those are used in a.АкrокiaDeployedRelease AJ PanoramaAJREPORTSDeployed9-20740 05 1 ••*=summary in the CRMSteliyan GeorgievStetika Stoyanova4 othersNikolay YankovLukas Kovalik26:17...
|
6762
|
NULL
|
NULL
|
NULL
|
|
6762
|
293
|
8
|
2026-05-08T07:10:50.323905+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224250323_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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Show Replace Field
Search History
$endpoint
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/5
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
13
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $type, $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 $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} 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 {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} 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;
}
}
// RateLimitException and other exceptions are re-thrown as-is
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Show Replace Field","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Search History","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"$endpoint","depth":4,"on_screen":true,"value":"$endpoint","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Match Case","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Words","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Regex","depth":3,"on_screen":true,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Replace History","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextField","text":"Replace","depth":4,"on_screen":false,"role_description":"text field","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"New Line","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXCheckBox","text":"Preserve case","depth":3,"bounds":{"left":0.0,"top":0.0,"width":0.015277778,"height":0.024444444},"on_screen":false,"role_description":"checkbox","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1/5","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Occurrence","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Next Occurrence","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Filter Search Results","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Open in Window, Multiple Cursors","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXLink","text":"Click to highlight","depth":4,"on_screen":false,"role_description":"link","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Close","depth":4,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"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":"13","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, $type, $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 $objectType, array $payload, PaginationState $state): array\n {\n try {\n return $client->search($objectType, $payload);\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 $result = $client->search($objectType, $payload);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $result;\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 }\n\n // RateLimitException and other exceptions are re-thrown as-is\n throw $e;\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, $type, $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 $objectType, array $payload, PaginationState $state): array\n {\n try {\n return $client->search($objectType, $payload);\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 $result = $client->search($objectType, $payload);\n\n $this->logger->info('[Hubspot] Token refresh and retry successful', [\n 'team_id' => $client->getConfig()->getTeam()->getId(),\n ]);\n\n return $result;\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 }\n\n // RateLimitException and other exceptions are re-thrown as-is\n throw $e;\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}]...
|
3401949268335374344
|
-2274930371109148689
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Show Replace Field
Search History
$endpoint
New Line
Match Case
Words
Regex
Replace History
Replace
New Line
Preserve case
1/5
Previous Occurrence
Next Occurrence
Filter Search Results
Open in Window, Multiple Cursors
Click to highlight
Close
Sync Changes
Hide This Notification
Code changed:
Hide
13
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm\Hubspot\Pagination;
use Jiminny\Exceptions\RateLimitException;
use Jiminny\Services\Crm\Hubspot\Client;
use Jiminny\Services\Crm\Hubspot\PayloadBuilder;
use Psr\Log\LoggerInterface;
use SevenShores\Hubspot\Exceptions\BadRequest;
use SevenShores\Hubspot\Exceptions\HubspotException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
class HubspotPaginationService
{
public function __construct(
private LoggerInterface $logger
) {
}
/**
* @throws HubspotException
* @throws SocialAccountTokenInvalidException
* @throws BadRequest
*/
public function getPaginatedDataGenerator(
Client $client,
array $payload,
string $type,
int $offset = 0,
int &$total = 0,
?string &$lastRecordId = null
): \Generator {
$state = new PaginationState(offset: $offset);
$endpoint = Client::BASE_URL . "/crm/v3/objects/{$type}/search";
$defaultFilter = $payload['filters'] ?? [];
$resultsPerPage = PayloadBuilder::MAX_SEARCH_REQUEST_LIMIT;
$teamId = $client->getConfig()->getTeam()->getId();
$delay = $this->calculateDelayInMicroseconds();
do {
if ($this->shouldStopPagination($state, $teamId)) {
break;
}
$payload = $this->handlePaginationStrategy($payload, $defaultFilter, $state, $resultsPerPage, $teamId);
$this->validateTokenIfNeeded($client, $state);
usleep($delay);
$page = $this->executeSearchRequest($client, $type, $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 $objectType, array $payload, PaginationState $state): array
{
try {
return $client->search($objectType, $payload);
} 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 {
$result = $client->search($objectType, $payload);
$this->logger->info('[Hubspot] Token refresh and retry successful', [
'team_id' => $client->getConfig()->getTeam()->getId(),
]);
return $result;
} 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;
}
}
// RateLimitException and other exceptions are re-thrown as-is
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...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
6760
|
293
|
7
|
2026-05-08T07:10:43.309638+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224243309_m1.jpg...
|
PhpStorm
|
faVsco.js – Client.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
master, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"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}]...
|
-7673782238848625796
|
-8646559087753982588
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
master, menu
FirefoxFileE Project: faVsco.js, menu
master, menu
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)51 Servicev Planha XX Jmine83 PromelE DatadogPlatform Team %.Q Search boards009 J1-19S09 1 J7-20301KAOTTOKOEYAJ Panorama for Call Scoring in ODSetup test coverage forMAINTENANCEDockoeE JY-19951***= 3~ DescriptionCurrently AJ Panorama in OD allows managers to get insights about the performance of their team. We have now added afunctionality for Al Call Scoring that also provides insights about the performance. It wil be good to leverage both of themin the chat so that managers can get the best results.• Include the Call Score information for each call in Panorama• the context should include - total score, score card name, individual rules score, scoring criteria (the customer'sprompt for the rufe), justification, timestamp. This information is stored in the ESSubtasksAdd subtaskLinked work itemsAdd linked work itecActivityHsoryWvotk. 1oalSuggest a replyStatus update...Thanks..Pro tipc press M to comment)*3 Calis -Aner83 МСРnsights & Coachin.I Improve Story~ DetailsAssignee3 Steliyan GeorgievAssign to me& Galya Dimitrova• Quick start developmentUink this work item to your codeby including keys when creatinga branch, commit, or pull requestbelow. Learn moreDismissDevelo omenOpen with VS CodeIJ Create branch4 Create commitComponentsPlatformSub-ProductAdd optionsC Dev10:10 AM | Daily - PlatformSupport Daily • in 4 h 50 m100% <2Fri 8 May 10:10:448• Fri 8 May 10:10L Al BookmarksGroup: QueriesAl Reports > Empty pagedesign and promotonAJREPORTSDeployed9 -20372 |1 11 •***=GrOK Va AZUCД Jy-20726 |1 • ***=Allow users to delete SSand Panorama promptswhen those are used in a.АкrокiaDeployedRelease AJ PanoramaAJREPORTSDeployed0-20740 05 1) •.0•:weeinoosummary in the CRMNikolay YankovStefka Stoyanova4 othersGalya DimitrovaLukas Kovalik25:28...
|
6758
|
NULL
|
NULL
|
NULL
|
|
6758
|
293
|
6
|
2026-05-08T07:10:30.544091+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224230544_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"Project","depth":3,"on_screen":false,"role_description":"text"},{"role":"AXButton","text":"Project","depth":3,"on_screen":true,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"New File or Directory…","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Expand Selected","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Collapse All","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Options","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
6757
|
293
|
5
|
2026-05-08T07:10:25.172734+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224225172_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.php
|
True
|
NULL
|
monitor_1
|
NULL
|
NULL
|
NULL
|
NULL
|
Project: faVsco.js, menu
|
[{"role":"AXButton","text" [{"role":"AXButton","text":"Project: faVsco.js, menu","depth":5,"on_screen":true,"help_text":"~/jiminny/app","role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false}]...
|
8043719072324535154
|
-8628527368849355612
|
click
|
hybrid
|
NULL
|
Project: faVsco.js, menu
FirefoxFileEditViewHistor Project: faVsco.js, menu
FirefoxFileEditViewHistoryBookmarksProfilesToolsWindowHelpmeet.google.com/agt-teir-cwt?authuser=lukas.kovalik%40jiminny.comNikolay Yankov (Presenting)$C7 Servicev Planha: XI• Planha X | E3 Jminn83 Promo:https:/fiminny.atlassian.net/jra/software/c/projects/JY/boards/37?selectedissue=JY-20361E DatadogPlatform Team %.Q Search boardS009JT-19S09 W J7-Z0301KADTTOKOYAJ Panorama for Call Scoring in ODSetup test coverage forMAINTENANCIDackheE JY-199511****= 3~ DescriptionCurrently AJ Panorama in OD allows managers to get insights about the performance of their team. We have now added afunctionality for Al Call Scoring that also provides insights about the performance. It wil be good to leverage both of themin the chat so that managers can get the best results.• include the Call Score information for each call in Panorama• the context should include - total score, score card name, individual rules score, scoring criteria (the customer'sprompt for the rufe), justification, timestamp. This information is stored in the ESSubtasksAdd subtaskLinked work itemsAdd linked work ibecrActivityHsoyWotk. loalSuggest a replyStatus update...UnenkaseePro tipc press M to con*0 Calls -Atten83 МСР*XIInsights & Coachin…In DevI Improve StoryDetailsAssignee3 Stellyan GeorgievAssign to me& Gatya DimtrovaQuick start development|Uink this work item to your codeby including keys when creatinga branch, commit, or pull requestbelow. Learn moreDismissDevelo omenOpen with VS CodeIJ Create branch4 Create commitComponentsPlatformSub-ProductlAdd options0 Dew10:10 AM | Daily - PlatformSupport Daily • in 4 h 50 m100% 12Fri 8 May 10:10:2598• Fi8 May 10:10L Al BockmarxsGroup: Queries%Al Reports > Empty pagedesign and promotionAJREPORTSDeployed0 -20372 1 11 .**=GOK M AZUCД л7-20728 1 0 •**=Allow users to deiete SSand Panorama promptswhen those are used in a.ARIrКiSDeployed207201 82 900sRelease AJ PanoramaAJREPORTSDeployed0-20740 05 12 •**summary in the CRMNikolay YankovStefka Stoyanova4 othersGalya DimitrovaLukas Kovalik25:09...
|
6735
|
NULL
|
NULL
|
NULL
|
|
6755
|
293
|
4
|
2026-05-08T07:09:54.849670+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224194849_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6735
|
NULL
|
NULL
|
NULL
|
|
6753
|
293
|
3
|
2026-05-08T07:09:24.260174+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224164260_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6735
|
NULL
|
NULL
|
NULL
|
|
6751
|
293
|
2
|
2026-05-08T07:08:53.613327+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224133613_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6735
|
NULL
|
NULL
|
NULL
|
|
6749
|
293
|
1
|
2026-05-08T07:08:23.062994+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224103062_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6735
|
NULL
|
NULL
|
NULL
|
|
6747
|
293
|
0
|
2026-05-08T07:07:52.410659+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224072410_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6735
|
NULL
|
NULL
|
NULL
|
|
6742
|
292
|
7
|
2026-05-08T07:06:46.523261+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778224006523_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6740
|
292
|
6
|
2026-05-08T07:06:15.853316+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223975853_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6738
|
292
|
5
|
2026-05-08T07:05:45.138915+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223945138_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6736
|
292
|
4
|
2026-05-08T07:05:14.417531+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223914417_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6734
|
292
|
3
|
2026-05-08T07:04:43.740951+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223883740_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6732
|
292
|
2
|
2026-05-08T07:04:12.811933+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223852811_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6730
|
292
|
1
|
2026-05-08T07:03:42.170757+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223822170_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6728
|
292
|
0
|
2026-05-08T07:03:11.544543+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223791544_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6741
|
291
|
7
|
2026-05-08T07:06:36.298849+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223996298_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6735
|
NULL
|
NULL
|
NULL
|
|
6739
|
291
|
6
|
2026-05-08T07:06:02.926987+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223962926_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6735
|
NULL
|
NULL
|
NULL
|
|
6737
|
291
|
5
|
2026-05-08T07:05:31.435128+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223931435_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6735
|
NULL
|
NULL
|
NULL
|
|
6735
|
291
|
4
|
2026-05-08T07:05:00.563299+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223900563_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
NULL
|
NULL
|
NULL
|
NULL
|
|
6733
|
291
|
3
|
2026-05-08T07:04:29.671751+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223869671_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected...
|
[{"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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
-7779995999641515225
|
931051297686370526
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected...
|
6705
|
NULL
|
NULL
|
NULL
|
|
6731
|
291
|
2
|
2026-05-08T07:03:59.092464+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223839092_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6705
|
NULL
|
NULL
|
NULL
|
|
6729
|
291
|
1
|
2026-05-08T07:03:28.503068+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223808503_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6705
|
NULL
|
NULL
|
NULL
|
|
6727
|
291
|
0
|
2026-05-08T07:02:41.601073+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223761601_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6705
|
NULL
|
NULL
|
NULL
|
|
6724
|
290
|
8
|
2026-05-08T07:01:50.471283+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223710471_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6722
|
290
|
7
|
2026-05-08T07:01:19.490569+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223679490_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6720
|
290
|
6
|
2026-05-08T07:00:48.722430+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223648722_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6718
|
290
|
5
|
2026-05-08T07:00:18.082319+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223618082_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6716
|
290
|
4
|
2026-05-08T06:59:47.425058+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223587425_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6714
|
290
|
3
|
2026-05-08T06:59:16.794381+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223556794_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6712
|
290
|
2
|
2026-05-08T06:58:46.167622+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223526167_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6710
|
290
|
1
|
2026-05-08T06:58:15.459786+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223495459_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6708
|
290
|
0
|
2026-05-08T06:57:44.828266+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223464828_m2.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"bounds":{"left":0.4401596,"top":0.06624102,"width":0.31615692,"height":0.91300875},"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.042220745,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.27027926,"top":1.0,"width":0.008643617,"height":0.0},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"bounds":{"left":0.38763297,"top":0.22426178,"width":0.00731383,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","depth":4,"bounds":{"left":0.39694148,"top":0.22426178,"width":0.007978723,"height":0.015163607},"on_screen":true,"role_description":"text"},{"role":"AXButton","text":"Previous Highlighted Error","depth":4,"bounds":{"left":0.40658244,"top":0.22266561,"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.41389626,"top":0.22266561,"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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6704
|
NULL
|
NULL
|
NULL
|
|
6723
|
289
|
8
|
2026-05-08T07:01:40.241777+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223700241_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6705
|
NULL
|
NULL
|
NULL
|
|
6721
|
289
|
7
|
2026-05-08T07:01:09.583982+00:00
|
/Users/lukas/.screenpipe/data/data/2026-05-08/1778 /Users/lukas/.screenpipe/data/data/2026-05-08/1778223669583_m1.jpg...
|
PhpStorm
|
faVsco.js – CrmActivityService.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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
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":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXTextArea","text":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Component\\Utility\\Service;\n\nuse Illuminate\\Cache\\RateLimiter;\nuse Jiminny\\Contracts\\Http\\RateLimited;\nuse Jiminny\\Contracts\\Http\\RateLimitInterface;\n\nclass ProviderRateLimiter\n{\n protected RateLimiter $rateLimiter;\n\n public function __construct(RateLimiter $rateLimiter)\n {\n $this->rateLimiter = $rateLimiter;\n }\n\n public function canMakeRequest(RateLimited $provider): bool\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $key = $rateLimit->getKey();\n\n if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {\n return false;\n }\n }\n\n return true;\n }\n\n public function requestAvailableIn(RateLimited $provider): int\n {\n return $provider->getRateLimits()->isNotEmpty()\n ? $provider->getRateLimits()\n ->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))\n ->max()\n : 0\n ;\n }\n\n public function incrementRequestCount(RateLimited $provider): void\n {\n /** @var RateLimitInterface $rateLimit */\n foreach ($provider->getRateLimits() as $rateLimit) {\n $this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());\n }\n }\n}","role_description":"text entry area","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Sync Changes","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide This Notification","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":false,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Code changed:","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.088194445,"height":0.027777778},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXButton","text":"Hide","depth":4,"bounds":{"left":0.0,"top":0.0,"width":0.018055556,"height":0.026666667},"on_screen":false,"role_description":"button","is_enabled":true,"is_focused":false,"is_selected":false,"is_expanded":false},{"role":"AXStaticText","text":"1","depth":4,"on_screen":true,"role_description":"text"},{"role":"AXStaticText","text":"5","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;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\n }\n}","depth":4,"on_screen":true,"value":"<?php\n\ndeclare(strict_types=1);\n\nnamespace Jiminny\\Services\\Crm;\n\nuse Illuminate\\Support\\Collection;\nuse Jiminny\\Contracts\\Repositories\\TeamRepository;\nuse Jiminny\\Contracts\\Services\\Crm\\ServiceInterface;\nuse Jiminny\\Exceptions\\InvalidArgumentException;\nuse Jiminny\\Exceptions\\SocialAccountTokenInvalidException;\nuse Jiminny\\Models\\Account;\nuse Jiminny\\Models\\Activity;\nuse Jiminny\\Models\\Contact;\nuse Jiminny\\Models\\Lead;\nuse Jiminny\\Models\\Opportunity;\nuse Jiminny\\Models\\Participant;\nuse Jiminny\\Models\\Stage;\nuse Jiminny\\Models\\Team;\nuse Jiminny\\Models\\User;\nuse Jiminny\\Services\\ResolveTeamCrmConnection;\nuse Psr\\Container\\ContainerExceptionInterface;\nuse Psr\\Container\\NotFoundExceptionInterface;\nuse Psr\\Log\\LoggerInterface;\nuse Exception;\nuse Throwable;\n\nclass CrmActivityService\n{\n public function __construct(\n private readonly TeamRepository $teamRepository,\n private readonly CachedCrmServiceDecorator $decorator,\n private readonly EmailHelper $emailHelper,\n private readonly ResolveTeamCrmConnection $teamCrmResolver,\n private readonly LoggerInterface $logger,\n ) {\n }\n\n /**\n * Updates CRM data for an activity and its participants.\n *\n * NOTE: This method performs multiple database writes and should be called\n * within a transaction by the caller to ensure atomicity.\n *\n * @param Activity $activity\n * @param bool $remoteSearch\n *\n * @throws ContainerExceptionInterface\n * @throws NotFoundExceptionInterface\n * @throws Exception\n */\n public function updateCrmData(\n Activity $activity,\n bool $remoteSearch = false,\n ): void {\n $crmService = null;\n $participants = $activity->getParticipants();\n $team = $activity->getTeam();\n\n $prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);\n if ($prospectSearchStrategy->ignoreCrmMatchData()) {\n $this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [\n 'activity_id' => $activity->getId(),\n 'strategy' => get_class($prospectSearchStrategy),\n ]);\n\n return;\n }\n\n if ($remoteSearch) {\n try {\n $crmService = $this->teamCrmResolver->resolveForTeam($team);\n } catch (SocialAccountTokenInvalidException) {\n $this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n ]);\n }\n }\n\n $records = $this->updateParticipantsCrmData(\n team: $team,\n activity: $activity,\n participants: $participants,\n crmService: $crmService,\n );\n\n if (! empty($records)) {\n $activity->updateActivityCrmData($records);\n }\n\n $activity->refresh();\n }\n\n /**\n * @param Collection<Participant> $participants\n *\n * @throws Exception\n *\n * @return array{\n * Lead|null,\n * Account|null,\n * Opportunity|null,\n * Contact|null,\n * Stage|null,\n * string|null\n *}|array{}\n */\n private function updateParticipantsCrmData(\n Team $team,\n Activity $activity,\n Collection $participants,\n ?ServiceInterface $crmService = null,\n ): array {\n $matchedRecords = [];\n $matchedDomainRecords = [];\n\n $this->validateCrmConfiguration($activity);\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n foreach ($participants as $participant) {\n if ($this->shouldSkipParticipant($participant)) {\n continue;\n }\n\n if (! $this->shouldPerformLookup($participant, $team)) {\n $this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [\n 'activity_id' => $activity->getId(),\n 'team_id' => $team->getId(),\n 'email' => $participant->getEmailAddress(),\n ]);\n\n $this->attachUserIfExists($participant, $team);\n\n continue;\n }\n\n $records = $this->findCrmRecords($participant, $activity);\n\n if (! empty($records)) {\n $matchedRecords[] = $records;\n } else {\n $records = $this->findCrmDomainRecords(\n crmService: $crmService,\n participant: $participant,\n activity: $activity,\n );\n if (! empty($records)) {\n $matchedDomainRecords[] = $records;\n }\n }\n\n if (empty($records)) {\n continue;\n }\n\n try {\n $activity->updateParticipantCrmData($records, $participant);\n } catch (Throwable $ex) {\n $this->logger->error('[CrmActivityService] Failed to update participant CRM data', [\n 'activity_id' => $activity->getId(),\n 'participant_id' => $participant->getId(),\n 'exception' => $ex->getMessage(),\n ]);\n\n continue;\n }\n }\n\n $bestMatch = $this->getBestMatch(\n matchedRecords : $matchedRecords,\n matchedDomainRecords: $matchedDomainRecords,\n );\n\n $this->logger->info('[CrmActivityService] CRM matching completed', [\n 'activity_id' => $activity->getId(),\n 'participants_processed' => $participants->count(),\n 'exact_matches' => count($matchedRecords),\n 'domain_matches' => count($matchedDomainRecords),\n 'best_match_found' => ! empty($bestMatch),\n ]);\n\n return $bestMatch;\n }\n\n private function shouldPerformLookup(Participant $participant, Team $team): bool\n {\n if ($participant->hasEmailAddress()) {\n return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());\n }\n\n return true;\n }\n\n private function validateCrmConfiguration(Activity $activity): void\n {\n if ($activity->getCrm() === null) {\n throw new InvalidArgumentException('Cannot find CRM configuration');\n }\n }\n\n private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array\n {\n return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);\n }\n\n private function findCrmRecords(Participant $participant, Activity $activity): ?array\n {\n $records = null;\n\n if ($participant->hasEmailAddress()) {\n $records = $this->decorator->matchExactlyByEmail(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n }\n\n if (empty($records) && $participant->getPhoneNumber() !== null) {\n $records = $this->decorator->matchByPhone(\n phone: $participant->getPhoneNumber(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n if (empty($records) && $participant->getName() !== null) {\n $records = $this->decorator->matchByName(\n name: $participant->getName(),\n userId: $activity->getUser()->getId(),\n );\n }\n\n return $records;\n }\n\n private function shouldSkipParticipant(Participant $participant): bool\n {\n return $participant->hasUser();\n }\n\n private function attachUserIfExists(Participant $participant, Team $team): void\n {\n if ($participant->hasEmailAddress() === false) {\n return;\n }\n\n $user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());\n\n if ($user instanceof User) {\n $participant->user_id = $user->getId();\n $participant->save();\n }\n }\n\n private function findCrmDomainRecords(\n ?ServiceInterface $crmService,\n Participant $participant,\n Activity $activity,\n ): array {\n if ($participant->hasEmailAddress()) {\n $this->decorator->setConfiguration($activity->getCrm());\n $this->decorator->setCrmService($crmService);\n\n $records = $this->decorator->matchByDomain(\n email: $participant->getEmailAddress(),\n userId: $activity->getUser()->getId()\n );\n if (! empty($records)) {\n return $records;\n }\n }\n\n return [];\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}]...
|
680680676277418071
|
931068821152938206
|
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
Sync Changes
Hide This Notification
Code changed:
Hide
<?php
declare(strict_types=1);
namespace Jiminny\Component\Utility\Service;
use Illuminate\Cache\RateLimiter;
use Jiminny\Contracts\Http\RateLimited;
use Jiminny\Contracts\Http\RateLimitInterface;
class ProviderRateLimiter
{
protected RateLimiter $rateLimiter;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function canMakeRequest(RateLimited $provider): bool
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$key = $rateLimit->getKey();
if ($this->rateLimiter->tooManyAttempts($key, $rateLimit->getQuota())) {
return false;
}
}
return true;
}
public function requestAvailableIn(RateLimited $provider): int
{
return $provider->getRateLimits()->isNotEmpty()
? $provider->getRateLimits()
->map(fn (RateLimitInterface $rateLimit): int => $this->rateLimiter->availableIn($rateLimit->getKey()))
->max()
: 0
;
}
public function incrementRequestCount(RateLimited $provider): void
{
/** @var RateLimitInterface $rateLimit */
foreach ($provider->getRateLimits() as $rateLimit) {
$this->rateLimiter->hit($rateLimit->getKey(), $rateLimit->getWindow());
}
}
}
Sync Changes
Hide This Notification
Code changed:
Hide
1
5
Previous Highlighted Error
Next Highlighted Error
<?php
declare(strict_types=1);
namespace Jiminny\Services\Crm;
use Illuminate\Support\Collection;
use Jiminny\Contracts\Repositories\TeamRepository;
use Jiminny\Contracts\Services\Crm\ServiceInterface;
use Jiminny\Exceptions\InvalidArgumentException;
use Jiminny\Exceptions\SocialAccountTokenInvalidException;
use Jiminny\Models\Account;
use Jiminny\Models\Activity;
use Jiminny\Models\Contact;
use Jiminny\Models\Lead;
use Jiminny\Models\Opportunity;
use Jiminny\Models\Participant;
use Jiminny\Models\Stage;
use Jiminny\Models\Team;
use Jiminny\Models\User;
use Jiminny\Services\ResolveTeamCrmConnection;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use Exception;
use Throwable;
class CrmActivityService
{
public function __construct(
private readonly TeamRepository $teamRepository,
private readonly CachedCrmServiceDecorator $decorator,
private readonly EmailHelper $emailHelper,
private readonly ResolveTeamCrmConnection $teamCrmResolver,
private readonly LoggerInterface $logger,
) {
}
/**
* Updates CRM data for an activity and its participants.
*
* NOTE: This method performs multiple database writes and should be called
* within a transaction by the caller to ensure atomicity.
*
* @param Activity $activity
* @param bool $remoteSearch
*
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
* @throws Exception
*/
public function updateCrmData(
Activity $activity,
bool $remoteSearch = false,
): void {
$crmService = null;
$participants = $activity->getParticipants();
$team = $activity->getTeam();
$prospectSearchStrategy = ProspectSearchStrategyFactory::match($team);
if ($prospectSearchStrategy->ignoreCrmMatchData()) {
$this->logger->info('[CrmActivityService] Ignoring crm data because of prospect strategy', [
'activity_id' => $activity->getId(),
'strategy' => get_class($prospectSearchStrategy),
]);
return;
}
if ($remoteSearch) {
try {
$crmService = $this->teamCrmResolver->resolveForTeam($team);
} catch (SocialAccountTokenInvalidException) {
$this->logger->warning('[CrmActivityService] CRM token expired, falling back to local search', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
]);
}
}
$records = $this->updateParticipantsCrmData(
team: $team,
activity: $activity,
participants: $participants,
crmService: $crmService,
);
if (! empty($records)) {
$activity->updateActivityCrmData($records);
}
$activity->refresh();
}
/**
* @param Collection<Participant> $participants
*
* @throws Exception
*
* @return array{
* Lead|null,
* Account|null,
* Opportunity|null,
* Contact|null,
* Stage|null,
* string|null
*}|array{}
*/
private function updateParticipantsCrmData(
Team $team,
Activity $activity,
Collection $participants,
?ServiceInterface $crmService = null,
): array {
$matchedRecords = [];
$matchedDomainRecords = [];
$this->validateCrmConfiguration($activity);
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
foreach ($participants as $participant) {
if ($this->shouldSkipParticipant($participant)) {
continue;
}
if (! $this->shouldPerformLookup($participant, $team)) {
$this->logger->info('[CrmActivityService] Email domain belongs to the team, skipping crm lookup', [
'activity_id' => $activity->getId(),
'team_id' => $team->getId(),
'email' => $participant->getEmailAddress(),
]);
$this->attachUserIfExists($participant, $team);
continue;
}
$records = $this->findCrmRecords($participant, $activity);
if (! empty($records)) {
$matchedRecords[] = $records;
} else {
$records = $this->findCrmDomainRecords(
crmService: $crmService,
participant: $participant,
activity: $activity,
);
if (! empty($records)) {
$matchedDomainRecords[] = $records;
}
}
if (empty($records)) {
continue;
}
try {
$activity->updateParticipantCrmData($records, $participant);
} catch (Throwable $ex) {
$this->logger->error('[CrmActivityService] Failed to update participant CRM data', [
'activity_id' => $activity->getId(),
'participant_id' => $participant->getId(),
'exception' => $ex->getMessage(),
]);
continue;
}
}
$bestMatch = $this->getBestMatch(
matchedRecords : $matchedRecords,
matchedDomainRecords: $matchedDomainRecords,
);
$this->logger->info('[CrmActivityService] CRM matching completed', [
'activity_id' => $activity->getId(),
'participants_processed' => $participants->count(),
'exact_matches' => count($matchedRecords),
'domain_matches' => count($matchedDomainRecords),
'best_match_found' => ! empty($bestMatch),
]);
return $bestMatch;
}
private function shouldPerformLookup(Participant $participant, Team $team): bool
{
if ($participant->hasEmailAddress()) {
return $this->emailHelper->shouldPerformLookup($team, $participant->getEmailAddress());
}
return true;
}
private function validateCrmConfiguration(Activity $activity): void
{
if ($activity->getCrm() === null) {
throw new InvalidArgumentException('Cannot find CRM configuration');
}
}
private function getBestMatch(?array $matchedRecords, ?array $matchedDomainRecords): array
{
return RecordSelector::pickBestFromLists($matchedRecords, $matchedDomainRecords);
}
private function findCrmRecords(Participant $participant, Activity $activity): ?array
{
$records = null;
if ($participant->hasEmailAddress()) {
$records = $this->decorator->matchExactlyByEmail(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
}
if (empty($records) && $participant->getPhoneNumber() !== null) {
$records = $this->decorator->matchByPhone(
phone: $participant->getPhoneNumber(),
userId: $activity->getUser()->getId(),
);
}
if (empty($records) && $participant->getName() !== null) {
$records = $this->decorator->matchByName(
name: $participant->getName(),
userId: $activity->getUser()->getId(),
);
}
return $records;
}
private function shouldSkipParticipant(Participant $participant): bool
{
return $participant->hasUser();
}
private function attachUserIfExists(Participant $participant, Team $team): void
{
if ($participant->hasEmailAddress() === false) {
return;
}
$user = $this->teamRepository->findActiveTeamMemberByEmail($team, $participant->getEmailAddress());
if ($user instanceof User) {
$participant->user_id = $user->getId();
$participant->save();
}
}
private function findCrmDomainRecords(
?ServiceInterface $crmService,
Participant $participant,
Activity $activity,
): array {
if ($participant->hasEmailAddress()) {
$this->decorator->setConfiguration($activity->getCrm());
$this->decorator->setCrmService($crmService);
$records = $this->decorator->matchByDomain(
email: $participant->getEmailAddress(),
userId: $activity->getUser()->getId()
);
if (! empty($records)) {
return $records;
}
}
return [];
}
}
Project
Project
New File or Directory…
Expand Selected
Collapse All
Options
Hide...
|
6705
|
NULL
|
NULL
|
NULL
|